Compare commits

..
Author SHA1 Message Date
Louis Pontoise ce6de14024 feat: high interpolation thumbnails 2021-03-31 22:56:26 +09:00
1675 changed files with 44509 additions and 121816 deletions

No files matched your search

-193
View File
@@ -1,193 +0,0 @@
---
name: assets-optimization
description: Audit and optimize every image asset shipped with AltTab. Apply the right format per asset class (PDF for vectors, HEIC for raster) and the right post-processing (strip Figma cruft from PDFs, extract SF Symbols as minimal vector PDFs, encode raster sources to HEIC at q50 with visual review). Use whenever new assets are added, when the bundle size needs shrinking, or whenever you want a full assets audit.
---
# /assets-optimization — AltTab asset audit and optimization
## Goal
Every byte that ships in `AltTab.app/Contents/Resources/` should justify itself. Vectors stay vector, rasters compress to HEIC, and neither carries metadata, color profiles, accessibility tags, or producer signatures that AppKit doesn't use.
This skill applies a known-good pipeline to each asset class. It is opinionated about the right format and the right encoder for each kind of content.
## When to use
- A designer drops new exports into `~/Desktop/` or `resources/`.
- Someone asks "why is the bundle so big?".
- After adding a new icon, illustration, app icon variant, or menubar variant.
- Periodic audit when nothing else is broken.
## Step 1: Inventory
Run a one-shot enumeration so you know what you're working with:
```sh
find resources -type f \( -iname '*.pdf' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.heic' -o -iname '*.svg' -o -iname '*.icns' \) \
-exec ls -la {} \; | awk '{printf "%8d %s\n", $5, $9}' | sort -k2
```
Group what you see by directory. For AltTab the relevant buckets are:
- `resources/icons/menubar/` — small template icons shown in the macOS menubar.
- `resources/icons/tabs/` — Preferences sidebar icons (template, sized ~13pt).
- `resources/icons/permission-window/` — first-launch permission window icons (~32pt).
- `resources/icons/app/` — the macOS app icon (`.icns` + `.iconset/`). **Don't touch** — `.icns` is required by the bundle and Apple's tooling produces near-optimal output already.
- `resources/illustrations/` — the appearance-tab preview thumbnails. Raster (screenshots inside).
For each asset, decide what category it falls into:
| Source content | Right format | Why |
|---|---|---|
| Custom vector design (Figma/Sketch/Illustrator) | **PDF** | macOS 10.13 doesn't accept SVG; PDF is the universal vector container AppKit reads natively. |
| SF Symbol (Apple system glyph) | **Font glyph** in the bundled `SF-Pro-Text-Regular.otf` subset | Render via `NSImage.fromSymbol(.foo, pointSize:)` (or as text via the `Symbols` enum). Smaller than per-icon PDFs, picks up Apple's latest glyph refinements automatically when the developer updates SF Symbols.app. |
| Photographic / screenshot-heavy | **HEIC** | HEIC at q50 beats JPEG by ~30% at the same perceptual quality. |
| Tiny pixel-precise UI sprite | **PNG @2x** | Below ~40×40px the PDF overhead exceeds the bitmap savings. PNG wins. |
| App icon (the macOS bundle one) | **`.icns`** | Required by `CFBundleIconFile`. |
If an asset is in the wrong format, flag it. If it's in the right format but unoptimized, run the matching pipeline below.
## Step 2: Vector PDFs — Figma exports
Figma's "Export → PDF" output is bloated. For each menubar/illustration/icon vector PDF that came from Figma, you can strip ~50–75% of the bytes without losing a single rendered pixel.
What Figma adds that AppKit doesn't need:
1. **Embedded ICC color profile** (`/ICCBased ...`, ~3.2 KB compressed). Replace every `[/ICCBased N R]` reference with `/DeviceRGB` (or `/DeviceGray` for monochrome). Patches needed in:
- the page `Resources/ColorSpace` dict
- every Form XObject's `Resources/ColorSpace` dict (these are streams, not plain dicts — pikepdf's `pdf.objects` will only catch them if you accept both `Dictionary` and `Stream`)
- every Image XObject's direct `/ColorSpace` key
- every Shading dict inside `Pattern` entries (Figma's color icons use 8+ patterns, each with its own `/ColorSpace N R` reference)
2. **`/Metadata`** XMP packet (~830 B) — Figma's XML manifest.
3. **`/StructTreeRoot`, `/ParentTree`, `/StructElem`** — accessibility tags ("Document" / "Part" structural roles). AppKit's PDF renderer ignores them.
4. **`/Info` dict** — `Producer="Figma"`, `Title="Menubar 22x22@1x white"`. In Figma's exports the Info dict sometimes lives **inline inside the Catalog** rather than at the trailer level, so deleting `pdf.docinfo` isn't enough — also `del root[Name('/Info')]`.
5. **`/Lang`, `/MarkInfo`, `/Annots`, `/StructParents`, `/Tabs`** — empty or trivial page-level entries.
6. **`/ProcSet [/PDF]`** — deprecated since PDF 1.4.
Use the script:
```sh
python3 scripts/assets/optimize_figma_pdf.py resources/icons/menubar/*.pdf
```
It edits in place and prints the savings per file. After running, also pipe through `mutool clean -ggg -z` and `qpdf --object-streams=generate --recompress-flate --compression-level=9` for the final 1–2% squeeze.
Verify each file still renders by sips'ing it back to PNG and eyeballing:
```sh
for f in resources/icons/menubar/*.pdf; do
sips -s format png "$f" --out "/tmp/$(basename $f .pdf).png" -Z 300 >/dev/null 2>&1
done
```
Open the PNGs in Preview to confirm nothing visual changed.
## Step 3: SF Symbols via font subset
Every SF Symbol shipped in AltTab — switcher status icons, sidebar tab icons, button icons, permission/feedback icons — is rendered as a text glyph from a subsetted SF Pro Text font. There are no SF-Symbol PDFs in the bundle.
How it works: SF Symbols are glyphs in the Private Use Area of SF Pro Text. Apple's `SF-Pro-Text-Regular.otf` contains every symbol they've ever shipped. We subset it down to just the codepoints AltTab needs (currently ~36 glyphs, ~17 KB) into `resources/SF-Pro-Text-Regular.otf`, and register it via `Info.plist:ATSApplicationFontsPath = ""`. At runtime, `NSFont(name: "SF Pro Text", size:)` resolves to the bundled subset on macOS <11 (where the system font isn't installed) and to the system font on macOS 11+, with identical glyph appearance either way.
To add a new SF Symbol:
1. Open [SF Symbols.app](https://developer.apple.com/sf-symbols/), search for the symbol, press **Cmd-C** to copy the symbol character to the clipboard. (Apple's name→codepoint mapping is not exposed via public API, and the SF Pro Text font's cmap uses `uniXXXXXX.medium`-style names rather than semantic ones, so this manual lookup is the authoritative path.)
2. Paste the character into a new case on the `Symbols` enum in [src/switcher/main-window/TileFontIconView.swift](src/switcher/main-window/TileFontIconView.swift) — e.g., `case foo = "􀝥" // SF Symbol name`.
3. Paste the same character at the end of the `--text=` argument in [scripts/assets/subset_font.sh](scripts/assets/subset_font.sh).
4. Run `bash scripts/assets/subset_font.sh`. It reads `/Library/Fonts/SF-Pro-Text-Regular.otf` (installed by SF Symbols.app — a standard developer prerequisite) and writes the regenerated subset to `resources/SF-Pro-Text-Regular.otf`. Picks up Apple's latest glyph refinements automatically.
5. Use it in code: `NSImage.fromSymbol(.foo, pointSize: 14)` returns a template `NSImage`; or `TileFontIconView(symbol: .foo, ...)` for the cached-attributed-string path in the switcher hot loop.
The script runs `pyftsubset` via the project's pipenv environment. Warnings about `MERG`/`meta`/`trak` tables being dropped are normal — those tables aren't relevant to glyph rendering.
### Historical note: SF Symbols via PDF (deprecated, scripts removed)
A previous pipeline shipped each SF Symbol as a per-glyph PDF in `resources/icons/`. The pipeline lives only in git history now (`scripts/assets/export_sf_symbol_pdf.swift` was deleted alongside the migrated PDFs). The technique is worth knowing in case a future need arises (e.g., a multi-color symbol that fonts can't represent):
- The naive route — `NSImage(systemSymbolName:).draw(in:)` against a PDF `CGContext` — produces a **black rectangle**, because Quartz emits `image-mask + fill-rectangle` operators where the rectangle paints over the mask. AppKit bug at the PDF emission level; `paletteColors` config does not fix it.
- The working route was to extract the symbol's vector path directly via private selectors that have been stable across macOS 11–15:
`NSImage(systemSymbolName:).representations[0] (NSSymbolImageRep) → .perform("vectorGlyph") (CUINamedVectorGlyph) → .perform("CGPath") (real CGPath)`.
- The CGPath lives in CUI's internal coordinate space (~2× display points, Y-down) — scale to fit the canvas and flip Y. Walk the path via `CGPath.applyWithBlock` and emit raw PDF operators (`m`, `l`, `c`, `h`, `f`). Non-zero winding fill. Fill with DeviceGray (`0 g`), **not** `NSColor.black.cgColor`, which drags in a ~3.4 KB ICC color profile.
- Final PDF wrapper: 4 objects (Catalog, Pages, Page, Content), no `/Info`, no `/Metadata`, no `/Resources/ColorSpace`. ~750–1800 bytes per icon.
Git: see commit `990c1e79` ("feat: pro improve assets") for the PDF pipeline as-it-was; subsequent commit migrated the SF-Symbol PDFs back to font glyphs.
## Step 4: Raster → HEIC at q50
For anything raster (illustration thumbnails, screenshots inside an icon, anything photographic), HEIC at quality 50 is the baseline. q50 is roughly 50% smaller than JPEG at perceptually-equivalent quality, and at the small display sizes used in this app the artifacts are invisible.
Pipeline (built-in to macOS via `sips`):
```sh
sips -Z 1000 -s format heic -s formatOptions 50 input.png --out output.heic
```
- `-Z 1000` resizes the longest edge to 1000px **preserving aspect ratio**. AltTab's illustration display is 500pt wide, so 1000px is the correct @2x ship size. Anything larger wastes bytes; anything smaller looks soft on Retina.
- `formatOptions 50` is the quality. q50 was chosen after a side-by-side comparison at q20/q35/q50/q65/q80 — q50 was the lowest setting where text in screenshots stayed legible and gradient backgrounds didn't band.
Use the script for batch conversion:
```sh
bash scripts/assets/encode_heic.sh ~/Desktop 1000 50
```
That walks all PNG/JPEG files in the source directory, resizes to longest-edge 1000px at q50 HEIC, writes outputs to `/tmp/heic-out/`.
### Visual review (mandatory)
Before swapping the new HEICs into `resources/`, **always** decode a representative sample back to PNG and visually compare against the source:
```sh
sips -s format png /tmp/heic-out/sample.heic --out /tmp/sample-decoded.png >/dev/null 2>&1
open /tmp/sample-decoded.png /Users/you/Desktop/sample.png
```
Pick the visually most demanding file from the batch — usually one with the most text or the strongest gradients. Confirm:
- No banding in flat color regions
- Text edges still crisp at native display size
- No haloing around anti-aliased edges
- Color rendition matches
If anything looks degraded, bump quality to q60 or q65 and re-batch. The user, not the script, is the final arbiter — show them the sample with sizes before committing.
### Bumping quality
If q50 isn't acceptable, the next quality steps are q60 and q65 — beyond that, returns diminish quickly. q80 is the previous default in this repo and roughly 2× the bytes of q50 for no visible improvement on AltTab's content.
## Step 5: pbxproj registration
Whenever you change the file extension of a resource (.jpg → .heic, .png → .pdf, etc.), update [alt-tab-macos.xcodeproj/project.pbxproj](alt-tab-macos.xcodeproj/project.pbxproj). The places that need patching:
1. **PBXBuildFile section** — comment + the comment inside `fileRef = ... /* name.ext */`.
2. **PBXFileReference section** — comment, `lastKnownFileType` (e.g. `image.pdf`, `image.heic`, `image.png`), and `path = "name.ext"`.
3. **PBXGroup section** — the file's entry inside its parent group's `children`.
4. **PBXResourcesBuildPhase section** — the entry in the main app target's `files`.
For pure extension swaps (no new files), `sed -i '' 's|old\.ext|new.ext|g'` plus a `lastKnownFileType` substitution covers it. For new files, generate new 24-char uppercase-hex object IDs (`python3 -c "import secrets; print(secrets.token_hex(12).upper())"`) and insert in all four places.
If you removed an asset entirely (file deleted from disk), delete its 4 entries from pbxproj — otherwise the build fails with "missing file" or ships dangling references.
## Step 6: Verify
```sh
bash ai/build.sh # must show ** BUILD SUCCEEDED **
bash ai/run.sh # launch the app and visually inspect every asset
```
Walk through every UI surface that loads an asset:
- Menubar icon (default + the two alternates from Preferences → General → Menubar icon)
- Preferences sidebar — 4 tab icons (SF Symbol on macOS 11+, bundled PDF below)
- Permissions window — open by revoking a permission
- Preferences → Appearance — illustration thumbnails change per show/hide row
Compare `git diff --stat` before committing. Asset replacements should net negative on bundle size.
## Reporting
After the run, report:
- File-by-file before/after sizes for everything that changed.
- Total bundle delta in KB.
- Any files left untouched and why (e.g., `app.icns` — bundle-required format).
- The encoder settings used (especially HEIC quality if not q50, so the next person knows).
- Anything the visual review revealed (e.g., "had to bump to q60 for `thumbnails_dark` because gradient banding at q50").
-65
View File
@@ -1,65 +0,0 @@
---
name: audit-specs-tests
description: Audit the AltTab test suite for spec/test/code consistency. For every co-located triad in `src/` (`Foo.swift` + `FooSpecs.md` + `FooTests.swift`), it cross-checks that each XCTest method has a matching scenario line in the spec and vice-versa, flags drift between a scenario's description and the test's doc-comment, and reports orphan tests, orphan scenarios, and unpaired files. Use after adding or editing any `*Tests.swift` / `*Specs.md`, before committing a test change, or to get an overall health check of the suite.
---
# /audit-specs-tests
## Project context
AltTab co-locates tests and a living spec next to the source they cover. The convention (see the plan in `.claude/plans/` and `src/pro/license/` as a worked example) is a **triad** per concept:
```
src/<feature>/
├── Foo.swift # production / kernel (app target, or app+test for kernels)
├── FooSpecs.md # the living spec (not compiled)
└── FooTests.swift # XCTest (unit-tests target only)
```
The spec is **documentation tied 1:1 to the test methods**. Its `## Test scenarios` section lists every test, grouped, as:
```markdown
### A. <Group name>
- **testAFooDoesBar** — one-line description of what it pins.
```
This skill's job is to keep the three in sync: every `func test…` ⇔ exactly one `- **test…** —` spec line, descriptions roughly agree, and no triad is missing a leg.
## What counts as a triad
Pair by base name in the **same folder**: `<Base>Tests.swift` ⇔ `<Base>Specs.md`. A `<Base>.swift` (or a kernel like `SelectionResolver.swift`, `SearchTestable.swift`) in the folder is the code under test, but a spec may legitimately cover several classes (e.g. `LicenseManagerSpecs.md` documents `Clock`/`Keychain`/`LicenseAPI` too) — so a missing same-name `.swift` is a note, not an error.
## Workflow
1. **Enumerate.** Find all test and spec files:
```sh
find src -name '*Tests.swift' | sort
find src -name '*Specs.md' | sort
```
Also list any test files still under `unit-tests/` and flag them as **not yet migrated** (the target end-state is zero test files outside `src/`).
2. **For each `*Tests.swift`, extract the test methods and their doc-comments.** Test names:
```sh
grep -nE '^\s*func test[A-Za-z0-9_]+\(' src/<path>/FooTests.swift
```
For each, capture the immediately-preceding `///` or `//` comment lines (the test's intent) and the enclosing `// MARK: -` group. Note methods wrapped in `#if DEBUG`.
3. **For each `*Specs.md`, parse the scenario index.** In the `## Test scenarios` section, collect every `### <Group>` heading and every `- **testName** — description` bullet. Build the set of spec-listed test names and their descriptions.
4. **Cross-check each triad and classify findings:**
- **Orphan test** — `func testX` exists but no `- **testX** —` line in the spec. (Most common drift; spec is stale.)
- **Orphan scenario** — spec lists `testX` but no `func testX` in the suite. (Test deleted/renamed; spec stale.)
- **Group mismatch** — a test's `// MARK:` group and its spec `###` group disagree (readability drift, not fatal).
- **Description drift** — the spec bullet and the test's doc-comment describe materially different behavior. Judge semantically, don't string-match; only flag real divergence.
- **Unpaired file** — a `*Tests.swift` with no `*Specs.md` (or vice-versa).
5. **Lighter code check (kernels only).** For decision-kernel files (`*Testable.swift`, `*Resolver.swift`), list `public`/`static` entry-point functions and `enum … Decision` cases, and flag any with no scenario referencing them — a hint that a branch is undocumented/untested. Skip this for AppKit-coupled production files (intentionally not unit-tested here).
6. **Optionally repair.** If asked, regenerate a spec's `## Test scenarios` section from the live test methods + their doc-comments (preserving the existing `### Group` order where it matches `// MARK:` sections). Never invent behavior — only restate what the test asserts. Do not edit `*Tests.swift` from this skill.
## Reporting
- A per-triad status table: `folder/Base` · #tests · #scenarios · status (✅ in sync / ⚠️ drift / ❌ unpaired).
- A punch list grouped by finding type (orphan tests, orphan scenarios, description drift, unpaired, undocumented kernel branches), each with the file + line.
- The count of test files still outside `src/` (migration progress).
- If nothing is wrong, say so plainly. Then offer to fix the drift (regenerate stale spec scenario indexes) and list exactly which files would change.
-63
View File
@@ -1,63 +0,0 @@
---
name: coverage-explore
description: Measure AltTab test coverage and record it as documentation. Runs the Test scheme with coverage into a throwaway result bundle, parses the per-file/region report, refreshes the per-feature badge atop each `*Specs.md` and the root `src/coverage.md` table, then suggests the highest-value missing tests. These two markdown outputs are the ONLY stored coverage — no lcov/html/xcresult is kept, and routine test runs don't gather coverage. Use to refresh coverage docs, find untested kernel branches, or decide what to test next.
---
# /coverage-explore
## Project context
AltTab follows the **Humble Object** pattern: logic lives in dependency-free **decision kernels**
(`SelectionResolver.swift`, `SearchModeResolver.swift`, the `*Testable.swift` files) compiled into the
`unit-tests` target and tested in isolation; the AppKit layer (windows, views, event taps) is kept thin
and is deliberately *not* unit-tested. So coverage is meaningful **per kernel / per feature-source** —
those should trend to ~100%, every decision branch exercised — and an aggregate app-wide % is not a
goal.
**Coverage is documentation here, not a build artifact.** The only stored coverage is:
1. the per-feature badge (`> **Line coverage:** …`) atop each `*Specs.md`, and
2. the root **`src/coverage.md`** table.
There is intentionally **nothing else**: no lcov, no HTML, no committed `.xcresult`, and coverage is
**not** gathered on routine runs (`scripts/run_tests.sh`, CI, or Xcode) — only when this skill runs it.
(Coverage isn't usefully viewable in IntelliJ, so there's no IDE-integration artifact to maintain.)
## Workflow
1. **Measure into a throwaway bundle** (then delete it — store nothing technical):
```sh
rm -rf /tmp/altcov.xcresult
set -o pipefail && xcodebuild test \
-project alt-tab-macos.xcodeproj -scheme Test -configuration Release \
-enableCodeCoverage YES -resultBundlePath /tmp/altcov.xcresult | scripts/xcbeautify
```
If the build is red, stop and report — coverage on a failing suite is meaningless.
2. **Parse** `xcrun xccov view --report --json /tmp/altcov.xcresult`. Each `targets[].files[]` has
`path`, `lineCoverage`, `coveredLines`, `executableLines`, `functions[]`. For line-by-line counts of
one file while investigating a gap: `xcrun xccov view --file <abs-path> /tmp/altcov.xcresult`.
3. **Refresh `src/coverage.md`.** Rewrite the table with every `src/` file (excluding `*Tests.swift`)
that the test target compiles, sorted worst-coverage-first, as `| NN% (cov/exec) | \`path\` |`.
Keep the header note explaining the Humble-Object scope and that this + the badges are the only
stored coverage. Update the "updated" date.
4. **Offer to refresh the per-spec badges.** For each `<Base>Specs.md` whose `<Base>.swift` (or kernel)
is in the report, propose updating its `> **Line coverage:** …` line with the new % + today's date.
Ask first; only apply the ones the user accepts. Keep honest notes where a file is intentionally
partial (e.g. `PreferencesMigrations.swift`).
5. **Rank gaps & suggest tests.** For kernels below 100%, list the uncovered functions/regions, map each
to the behavior it represents (read the source), and write ready-to-paste additions: a
`- **testGroupScenario** — …` spec line + a short XCTest stub in that suite's builder style. Don't add
tests automatically unless asked.
6. **Clean up:** `rm -rf /tmp/altcov.xcresult`. Leave no technical coverage files in the tree.
## Reporting
- Headline: a small ascending table of kernel `lineCoverage`, and the delta vs the previous `src/coverage.md`.
- Ranked gap list: file → uncovered function/branch → the behavior it represents.
- Suggested scenarios (spec lines + test stubs) for the top gaps.
- State exactly what was rewritten (`src/coverage.md`, which badges). Flag files that are partial *by
design* (AppKit glue, excluded migrations) so a low number isn't mistaken for a problem.
@@ -1,75 +0,0 @@
---
name: translate-missing-l10n
description: Translate missing l10n strings for AltTab using Claude's multilingual ability. Refreshes the source `Localizable.strings` from current Swift code, finds keys missing in each `<lang>.lproj/Localizable.strings`, translates them, then writes them back via `scripts/l10n/apply_translations.ts`.
---
# /translate-missing-l10n
## Project context
AltTab is a macOS app that helps switch between windows, similar to the Windows alt-tab experience. The strings to translate are mostly Settings UI — some tooltips, dialogs, and general user guidance.
## Target languages (20)
`de, ja, fr, es, zh-CN, pt-BR, nl, ko, it, pl, ar, zh-HK, vi, tr, sv, th, zh-TW, he, id, ru`
(`en` is the source — handled automatically by the apply step.)
## Translation guidelines
- **Format specifiers must be preserved exactly**: `%@`, `%d`, `%1$@`, `%2$@`, `\n`, `\t`. The number and order of specifiers in the translation must match the source. Positional and unindexed forms are interchangeable (`%@ %@` ≡ `%1$@ %2$@`), but the count must match.
- **Do not translate proper nouns**: "AltTab", "macOS", "Mission Control", "Spaces", "Dock".
- **Do not translate modifier key names**: "Cmd", "Option", "Alt", "Shift", "Ctrl", "Control", "Fn", "Command".
- **Use Apple's official platform terminology** for the target locale where one exists. For example, prefer the term Apple uses in macOS System Settings for that locale ("Réglages" vs "Préférences" in French) over a literal translation.
- **Match the source brevity**. Settings strings are short; the translation should be short too. Prefer concise, idiomatic phrasing over literal grammatical completeness.
- **Match macOS tone**: neutral, direct, no exclamation marks unless the source has them.
- **Comments in the source file are engineer guidance, not user-visible text**. Use them to disambiguate meaning, but never include them in the translation.
## Workflow
1. **Refresh the source.** Run:
```sh
bash scripts/l10n/extract_l10n_strings.sh
```
This regenerates `resources/l10n/Localizable.strings` from the current Swift code via `genstrings`.
2. **Parse the source.** Read `resources/l10n/Localizable.strings`. Each entry has the shape:
```
/* engineer comment */
"key" = "value";
```
Build the ordered list of `(comment, key, value)` triples. The `value` for the source is usually identical to `key`, but may include positional indices (`%1$@`, `%2$@`) when there are multiple specifiers.
3. **Compute missing keys per language.** For each of the 20 target languages, read `resources/l10n/<lang>.lproj/Localizable.strings`. Each line is `"key" = "translation";`. A key is **missing** if it's present in the source but either (a) absent from the target file, or (b) present with an empty or whitespace-only value. Treat (b) the same as (a) — produce a real translation.
4. **Stop if nothing is missing.** Report and exit.
5. **Translate.** For each language, produce a translation for each missing key, applying the guidelines above. Do not invent keys; only translate keys that appear in the source. Group your work into batches of 5–10 languages per call to keep individual outputs manageable.
6. **Apply each batch.** Write the batch to a fresh file under `/tmp` (not committed) with this shape:
```json
{
"fr": { "About %@": "À propos de %@", "Quit": "Quitter" },
"ja": { "About %@": "%@について" }
}
```
Then run:
```sh
npx ts-node scripts/l10n/apply_translations.ts /tmp/batch-NN.json
```
The helper:
- Validates format specifiers — translations whose specifier set doesn't match the source value are rejected with a clear error and **not merged**.
- Always rewrites `en.lproj/Localizable.strings` from source **keys** (each entry written as `"key" = "key";` for symmetry) — no need to include `en` in your batch. This avoids leaking genstrings-rewritten values like `%1$@` into the English file when the original source key uses plain `%@`.
- Rewrites each `<lang>.lproj/Localizable.strings` using source order: existing translations are preserved, new translations merged in, and keys no longer in the source are pruned.
- Exits with code `2` if any entries were rejected.
7. **Handle rejections.** If `apply_translations.ts` reports format-specifier mismatches, fix those translations and re-run only the affected language(s) in a follow-up batch. Do not move on while rejections are outstanding.
## Reporting
After the run, report:
- Number of languages processed.
- Total translations produced and merged.
- Any entries you intentionally left untranslated (e.g., the source was already a proper noun) — list them so the user can decide.
- Any format-specifier rejections that required manual fixes.
-1
View File
@@ -1 +0,0 @@
v11.3.0
-9
View File
@@ -1,9 +0,0 @@
# alt-tab-free [depaywall]: upstream's FUNDING.yml routed the GitHub "Sponsor" button on the fork's
# repo to lwouis (github: lwouis / patreon: alt_tab_macos / ko_fi: alt_tab / a lwouis PayPal button) —
# i.e. every donation would have gone to the author the fork just de-paywalled (PLAN §3.5). It has
# been emptied so NO sponsor button renders and no donation routes to the upstream author.
#
# OWNER-OVERRIDABLE-BEFORE-PUBLISH: to accept sponsorships for THIS fork, uncomment and fill in your
# own handles (do NOT re-add lwouis's). Otherwise leave this file empty / delete it.
# github: <fork-owner-handle>
# custom: ["https://<fork-domain>/donate"]
+1 -7
View File
@@ -13,7 +13,7 @@ A clear and concise description of what the bug is. What happened, and what you
**Screenshots / video**
Add screenshots to help explain your problem. You can also record your screen to show the bug in action. That is *really* helpful!
Add screenshots to help explain your problem. You can also use Loom to easily record your screen to show the bug in action. That is *really* helpful!
**Steps to reproduce the bug**
@@ -21,9 +21,3 @@ Add screenshots to help explain your problem. You can also record your screen to
2. Click on '....'
3. Scroll down to '....'
4. See error
**Your environment**
* AltTab version:
* macOS version:
* Other relevant info: e.g. using external keyboard, multiple monitors, etc
-226
View File
@@ -1,226 +0,0 @@
# alt-tab-free: fork-owned CI/CD workflow (diverged from upstream lwouis/alt-tab-macos).
# Upstream's ci_cd.yml fires `on: push: branches: [master]` ONLY and is the sole workflow,
# so this fork ADDS a `pull_request:` trigger to gate the sync PR pre-merge (see PLAN §4.2),
# gates every release/push-only step under `if: github.event_name == 'push'`, and wires the
# anti-relock + conflict-marker greps as the FIRST push-job steps so a relocked or conflicted
# tree aborts BEFORE update_appcast.sh (Sparkle sign), semantic-release (tag push), and the
# gh-release. This file is in the §6.2 "keep ours" recurring-conflict set — on an upstream
# ci_cd.yml change, reconcile manually and re-confirm the `pull_request:` trigger survives.
on:
push:
branches:
- master
# alt-tab-free [depaywall]: PR trigger so the sync PR runs the guards pre-merge (PLAN §4.2).
# A separate fork-owned .github/workflows/guard.yml ALSO runs the guards on pull_request, so a
# careless upstream-conflict resolution that strips this trigger cannot silently disarm the gate.
pull_request:
branches:
- master
# alt-tab-free [depaywall]: GitHub defaults GITHUB_TOKEN to read-only since 2023; semantic-release
# (@semantic-release/git) pushes commits+tags to master and softprops/action-gh-release creates the
# release — both need contents:write, else they 403 and the appcast enclosure 404s for auto-update
# (PLAN §4.3).
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPCENTER_SECRET: ${{ secrets.APPCENTER_SECRET }}
APPCENTER_TOKEN: ${{ secrets.APPCENTER_TOKEN }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_P12_CERTIFICATE: ${{ secrets.APPLE_P12_CERTIFICATE }}
# alt-tab-free [depaywall]: optional .p12 export password (empty if exported password-less).
# Not part of the release-gate AND — the cert import tolerates an empty password.
APPLE_P12_PASSWORD: ${{ secrets.APPLE_P12_PASSWORD }}
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
GITHUB_EVENT_AFTER: ${{ github.event.after }}
BUILD_DIR: ${{ github.workspace }}
XCODE_BUILD_PATH: DerivedData/Build/Products/Release
VERSION_FILE: VERSION.txt
# alt-tab-free [depaywall]: must equal PRODUCT_NAME in config/local.xcconfig
# (PLAN §3.1/§4.3) or the build/package/appcast scripts mismatch the artifact.
APP_NAME: CommandTabFree
jobs:
build:
runs-on: macos-15 # all options: https://github.com/actions/runner-images/tree/main
environment: production
steps:
# - env:
# GITHUB_CONTEXT: ${{ toJSON(github) }}
# run: echo "$GITHUB_CONTEXT"
- id: pre-checkout
env:
COMMITS: ${{ toJson(github.event.commits) }}
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
COUNT=$(( $(echo "$COMMITS" | jq length) + 30 ))
else
COUNT=1
fi
echo "fetch-depth=$COUNT" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v6
with:
fetch-depth: ${{ steps.pre-checkout.outputs.fetch-depth }}
# alt-tab-free [depaywall]: Guard B + Guard C run FIRST on the push job (before run_tests.sh,
# and therefore before update_appcast.sh's Sparkle sign + semantic-release's tag push) so a
# relock or a still-conflicted tree aborts the job BEFORE any irreversible side-effect (PLAN
# §4.4 "Where the guards run"). They are near-instant target-independent greps, so running them
# first is free. They also run on the PR path for the fastest red signal. A second copy sits
# just before action-gh-release as belt-and-suspenders.
- name: anti-relock — verify de-paywall marker present (Guard B)
run: |
grep -q 'depaywall' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: depaywall marker missing from computeState()"; exit 1; }
# The marker proves the patch is present; pair with the if-false wrap the patch introduces.
# A bare `return .pro` grep is NOT load-bearing (it matches the unpatched line 187 too).
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: 'guard false else' (forced .pro) missing from computeState()"; exit 1; }
- name: anti-conflict — no unresolved merge markers (Guard C)
run: |
if git -c grep.lineNumber=false grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- . ':(exclude)docs/'; then
echo "UNRESOLVED CONFLICT MARKERS present — resolve before merge/release"; exit 1
fi
# alt-tab-free [depaywall]: release-readiness gate (PLAN §3.3/§4.3 owner-secret checklist).
# The fork ships unsigned/ad-hoc until the OWNER provisions the Apple Developer ID cert and the
# Sparkle key. We do NOT hard-fail when those secrets are absent — that would make every push
# RED on a fork that is correctly built but not yet signing-provisioned. Instead we compute a
# boolean from secret presence and SKIP the sign/notarize/appcast/release tail cleanly when it
# is false. The build + Guard A (run_tests.sh) still run on push, so push CI is GREEN today and
# the release tail FLIPS ON automatically the moment the owner adds the secrets. Secrets are not
# available in step-level `if:` directly, so map them to env and emit an output here.
- name: release-gate — are signing/notarization secrets present?
id: release-gate
env:
HAS_P12: ${{ secrets.APPLE_P12_CERTIFICATE != '' }}
HAS_APPLE_ID: ${{ secrets.APPLE_ID != '' }}
HAS_APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD != '' }}
HAS_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID != '' }}
HAS_SPARKLE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY != '' }}
run: |
set -euo pipefail
if [ "$HAS_P12" = "true" ] && [ "$HAS_APPLE_ID" = "true" ] \
&& [ "$HAS_APPLE_PASSWORD" = "true" ] && [ "$HAS_APPLE_TEAM_ID" = "true" ] \
&& [ "$HAS_SPARKLE_KEY" = "true" ]; then
echo "signing-ready=true" >> "$GITHUB_OUTPUT"
echo "All signing/notarization/Sparkle secrets present — release tail WILL run on push."
else
echo "signing-ready=false" >> "$GITHUB_OUTPUT"
echo "::warning::Signing/notarization/Sparkle secrets absent — release tail (codesign/notarize/appcast/semantic-release/gh-release) is SKIPPED. Build + Guard A still run. Provision APPLE_P12_CERTIFICATE / APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID / SPARKLE_ED_PRIVATE_KEY in the 'production' Environment to enable releases (PLAN §3.3/§3.4)."
fi
- uses: actions/setup-node@v6
with:
node-version: 16 # PLAN §4.3: keep node-16; the package.json engines mismatch only warns
cache: 'npm'
- run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer
- run: scripts/print_env.sh
- run: npm ci
# alt-tab-free [depaywall]: commitlint is gated to the push path. On push it lints the whole
# pushed range, which drags in upstream's non-conforming messages on a `chore(sync)` merge —
# this is handled by an `ignores` predicate in commitlint.config.js (PLAN §4.3) that drops the
# synthetic merge commit and upstream's own messages; do NOT break commitlint on --no-ff merges.
# $GITHUB_EVENT_BEFORE/AFTER are push-context (empty on pull_request), so a PR-safe variant runs
# below on the PR path instead.
- run: npx commitlint --from "$GITHUB_EVENT_BEFORE" --to "$GITHUB_EVENT_AFTER" --verbose
if: github.event_name == 'push'
# alt-tab-free [depaywall]: PR-safe commitlint — lints base..head (valid on pull_request),
# paired with the commitlint.config.js `ignores` predicate (PLAN §4.2/§4.3).
- run: npx commitlint --from "${{ github.event.pull_request.base.sha }}" --to "${{ github.sha }}" --verbose
if: github.event_name == 'pull_request'
# Runs on both push and PR: catches an un-regenerated l10n/generated-file diff (PLAN §4.2/§4.5).
- run: scripts/ensure_generated_files_are_up_to_date.sh
# alt-tab-free [depaywall]: release-path — a semantic-release dry-run against a PR head is
# meaningless and can mis-bump, so gate to push (PLAN §4.2).
- run: scripts/determine_next_version.sh
if: github.event_name == 'push'
# alt-tab-free [depaywall]: decouple the release apparatus from "every signing-ready push".
# determine_next_version.sh writes an EMPTY VERSION.txt when no commit since the last tag is
# release-worthy (semantic-release dry-run finds no bump). Gate the sign/notarize/release steps
# on this so a docs/ci/chore push still runs guards + commitlint + run_tests (Release-config
# compile check) but does NOT sign-build, notarize, or cut a release — no [skip ci] needed (§4.3).
- name: release-decision — did semantic-release compute a version?
id: release-decision
if: github.event_name == 'push'
run: |
if [ -n "$(tr -d '[:space:]' < "$VERSION_FILE" 2>/dev/null)" ]; then
echo "will-release=true" >> "$GITHUB_OUTPUT"
echo "Release-worthy: semantic-release will cut v$(cat "$VERSION_FILE")."
else
echo "will-release=false" >> "$GITHUB_OUTPUT"
echo "::notice::No release-worthy commit — build/test only; notarize+release tail skipped."
fi
# Inject the release version only when releasing; an empty CURRENT_PROJECT_VERSION would crash
# App.version (see config/local.xcconfig). Non-release builds use the committed 0.0.0 default.
- run: scripts/replace_environment_variables_in_app.sh
if: github.event_name == 'push' && steps.release-decision.outputs.will-release == 'true'
# codesign needs APPLE_P12_CERTIFICATE — gated on signing-ready so a not-yet-provisioned fork
# does not RED on the missing-cert import (PLAN §3.3/§4.3).
- run: scripts/codesign/setup_ci_master.sh
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# Guard A — the anti-relock unit test (appended to LicenseManagerTests.swift). Runs on both
# push and PR (no secrets needed); on the push path it sits before the sign/tag steps below
# so a relock fails BEFORE any irreversible side-effect (PLAN §4.4). This is what keeps push
# CI GREEN today on the signing-unprovisioned fork.
- run: scripts/run_tests.sh
# alt-tab-free [depaywall]: build the Release product to prove the tree compiles. The fork
# inherits upstream's Developer ID via config/release.xcconfig (CODE_SIGN_IDENTITY), which is
# absent on CI until the owner provisions the Apple cert — so when the release gate is NOT
# signing-ready we build AD-HOC (CODE_SIGN_IDENTITY=-, signing not required, no timestamp) to
# prove the tree compiles without a cert. When the owner adds the secrets AND sets the fork's
# Developer ID in config/local.xcconfig, this builds signed for the release tail (PLAN §3.3/§4.3).
- name: build app (signed — owner cert provisioned)
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
run: scripts/build_app.sh
- name: build app (ad-hoc — until signing-provisioned)
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready != 'true'
run: scripts/build_app.sh CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO OTHER_CODE_SIGN_FLAGS=--timestamp=none
# --- Release tail: requires the owner's Apple Developer ID + Sparkle secrets. Skipped cleanly
# (NOT failed) when they are absent, so push CI stays GREEN until the owner provisions them. ---
- run: scripts/package_and_notarize_release.sh
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# alt-tab-free [depaywall]: upload_symbols_to_appcenter.sh REMOVED — telemetry is off (PLAN §3.4).
- run: scripts/update_appcast.sh
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# alt-tab-free [depaywall]: update_readme_and_website.sh REMOVED — it queried the UPSTREAM
# lwouis/alt-tab-macos GitHub API for stars/downloads and rewrote docs/contributors.md from the
# UPSTREAM contributor graph (semantic mismatch for the fork) and dispatched the lwouis website.
# Cosmetic only; dropped per PLAN §4.0/§4.3 (alongside update_website.sh, already removed).
- run: npx semantic-release
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
- id: set_release_info
run: scripts/extract_latest_changelog.sh
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
# alt-tab-free [depaywall]: belt-and-suspenders copy of Guard B + Guard C immediately before the
# release is created. The load-bearing copy is at the top of the job (pre-tag-push); this second
# copy guards the gh-release object itself (PLAN §4.4).
- name: anti-relock + anti-conflict re-check (belt-and-suspenders)
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
run: |
grep -q 'depaywall' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: depaywall marker missing from computeState()"; exit 1; }
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' src/pro/license/LicenseManager.swift \
|| { echo "PAYWALL RELOCKED: 'guard false else' (forced .pro) missing from computeState()"; exit 1; }
if git -c grep.lineNumber=false grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- . ':(exclude)docs/'; then
echo "UNRESOLVED CONFLICT MARKERS present — resolve before release"; exit 1
fi
# alt-tab-free [depaywall]: point the release at THIS fork's repo via the ambient GITHUB_TOKEN
# (release.config.js has no @semantic-release/github plugin, so softprops is the sole release
# creator — PLAN §4.3). repository defaults to ${{ github.repository }} = the fork.
- uses: softprops/action-gh-release@v3
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true' && steps.release-decision.outputs.will-release == 'true'
with:
repository: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ steps.set_release_info.outputs.tag_name }}
body: ${{ steps.set_release_info.outputs.body }}
files: ${{ env.XCODE_BUILD_PATH }}/*.zip
# alt-tab-free [depaywall]: the release tail intentionally does NOT advance .fork-sync-state.
# That file is the UPSTREAM-sync cursor — the last-synced UPSTREAM tag (e.g. v11.3.0), read by
# upstream_sync.yml to pick "the oldest upstream tag strictly newer than it." The fork versions
# independently (semantic-release cuts v100.x), so a prior revision's step that wrote the FORK
# release tag here set the cursor to v100.x — after which no upstream tag is ever "newer" and
# upstream-sync would silently never fire. The cursor must instead be advanced by the
# upstream-sync merge flow, recording the UPSTREAM tag it synced (to be wired with
# upstream_sync.yml — PLAN §4.2). Until then .fork-sync-state stays at the last real upstream tag.
# alt-tab-free [depaywall]: update_website.sh (lwouis website dispatch) REMOVED (PLAN §3.4/§4.2).
-94
View File
@@ -1,94 +0,0 @@
# alt-tab-free — anti-relock guard workflow (docs/PLAN-maintained-fork.md §4.4)
#
# A SEPARATE, fork-owned workflow that runs the anti-relock guards on every PR,
# INDEPENDENTLY of ci_cd.yml. Rationale (§4.4): ci_cd.yml is the file most likely to take an
# upstream conflict, and a careless "keep ours/theirs" resolution could silently strip its
# pull_request: trigger or its release-step gating — at which point the in-ci_cd.yml guards
# would stop gating PRs with NO error. A guard workflow that does NOT inherit ci_cd.yml's
# trigger survives that. This is the reviewer's primary green/red signal on a sync PR.
#
# Three guards, each fails the job LOUDLY:
# A — the Test scheme (the mock-injected anti-relock unit tests in LicenseManagerTests.swift)
# B — a target-independent grep that the de-paywall flip is still present in computeState()
# C — a tree-wide grep that no unresolved git conflict markers survive into a merge/release
#
# Defense-in-depth note: Guards B + C are ALSO wired into ci_cd.yml as the FIRST push-job
# steps (CI agent owns that) so a relock/conflict-laden tree aborts the release BEFORE the tag
# is pushed or the binary signed, even if this PR gate is ever missing/misconfigured (§4.4).
name: guard
on:
pull_request:
branches: [master]
permissions:
contents: read
jobs:
# Guards B + C are near-instant greps; run them on cheap ubuntu so a relock or a still-
# conflicted tree fails FAST, before the expensive macOS Guard-A job even starts (§4.5).
grep-guards:
name: anti-relock greps (B + C)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# GUARD B — verify the de-paywall flip is still present at the single state producer.
# §4.4 pitfall: a bare `return .pro` grep MATCHES THE UNPATCHED FILE (computeState() already
# contains `return .pro` in the keychain-valid branch on a fully relocked tree), so it is NOT
# load-bearing. The load-bearing signals are (1) the `depaywall` marker comment, which exists
# ONLY because the fork added it, AND (2) the `if false { … }` wrap that the patch introduces
# around the original (now dead) body. Both are absent on an unpatched/relocked upstream file.
- name: Guard B — de-paywall marker + if-false wrap present in computeState()
run: |
set -euo pipefail
F=src/pro/license/LicenseManager.swift
if [ ! -f "$F" ]; then
echo "::error::Guard B: $F not found — chokepoint moved? Refresh the de-paywall patch (§6.2)."
exit 1
fi
grep -q 'depaywall' "$F" || {
echo "::error::PAYWALL RELOCKED: 'depaywall' marker missing from $F — the de-paywall flip was lost in a merge. Refresh the patch (§6.2)."
exit 1
}
grep -Eq 'guard[[:space:]]+false[[:space:]]+else' "$F" || {
echo "::error::PAYWALL RELOCKED: 'if false { … }' wrap missing from $F — the de-paywall body-suppress was lost. Refresh the patch (§6.2)."
exit 1
}
echo "Guard B OK: de-paywall marker + if-false wrap present in $F."
# GUARD C — no unresolved git conflict markers anywhere in the tree. The §4.2 sync cron
# deliberately commits a conflicted tree (markers + --no-verify) for a human to resolve;
# nothing else guarantees the human removed EVERY marker before merge. A markers-laden
# commit merged to master becomes a permanent ancestor of the published tag (un-compilable
# source), contradicting §5(1). Exclude docs/ so this plan's own literal marker examples
# don't trip the grep.
- name: Guard C — no unresolved merge conflict markers
run: |
set -euo pipefail
if git -c grep.lineNumber=false grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- . ':(exclude)docs/'; then
echo "::error::UNRESOLVED CONFLICT MARKERS present — resolve before merge/release (§4.4 Guard C)."
exit 1
fi
echo "Guard C OK: no unresolved conflict markers."
# GUARD A — the behavioral anti-relock unit tests (mock-injected LicenseManager) via the Test
# scheme. §4.4: this MUST run on macos-15 with the pinned Xcode — run_tests.sh is `xcodebuild
# test`, which cannot run on ubuntu. If left on ubuntu, Guard A silently degrades to greps-only
# and the behavioral relock test never executes (the exact silent-disarming class this plan
# guards against). Bootstrap assertion (§4.4): on the deliberately-relocked acceptance PR,
# confirm THIS job produced a real test result that is RED on the relock — not just the greps.
unit-tests:
name: anti-relock unit tests (A)
runs-on: macos-15 # MUST match ci_cd.yml — `xcodebuild test` needs the pinned Xcode
steps:
- uses: actions/checkout@v6
# Pin the same Xcode as ci_cd.yml (§4.5 bootstrap invariant — re-pin on every upstream bump).
- run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer
# run_tests.sh runs the `Test` scheme (-configuration Release), which includes the
# appended anti-relock tests in src/pro/license/LicenseManagerTests.swift:
# testDepaywallProNeverLocked — load-bearing `state == .pro` assert
# testDepaywallStillProAfterTrialExpiry — discriminating (expired-trial) assert
# A relock makes computeState() fall back to computeTrialState() and these go RED.
- name: Guard A — run the Test scheme (anti-relock unit tests)
run: scripts/run_tests.sh
-226
View File
@@ -1,226 +0,0 @@
# alt-tab-free — upstream sync workflow (docs/PLAN-maintained-fork.md §4.2)
#
# Polls lwouis/alt-tab-macos for new release tags and opens a reviewable sync PR.
# Selects the OLDEST upstream tag strictly newer than the last-merged tag recorded in
# `.fork-sync-state` (NOT "newest tag minus --is-ancestor"). This is the §4.2
# never-drop-commits invariant: `.fork-sync-state` advances ONLY when a sync PR merges to
# master (a ci_cd.yml push-job step — owned by the CI agent), so a sync PR that is
# closed-without-merge or abandoned can never silently skip its tag — that tag stays "next"
# until it is actually merged. This subsumes the old `merge-base --is-ancestor` guard.
#
# A release is cut only when a human merges the PR to master (ci_cd.yml on: push). This
# workflow never releases; it only opens the PR for review.
name: upstream-sync
on:
# §4.2 cron-durability note: the in-repo `schedule:` SILENTLY auto-disables after 60 days
# of repo inactivity (no error, no notification). It is kept here as belt-and-suspenders.
# The RECOMMENDED PRIMARY trigger is an EXTERNAL scheduler hitting `repository_dispatch`
# carrying the SYNC_BOT_TOKEN (immune to the 60-day rule, and composes with the
# workflow-re-trigger token). See ownerActions / §4.2.
schedule:
- cron: '0 7 * * *' # daily; cheap, idempotent
workflow_dispatch: {}
repository_dispatch:
types: [upstream-sync] # external-scheduler entry point (§4.2 primary durability path)
# GITHUB_TOKEN is read-only by default; opening a PR + pushing a branch need these.
permissions:
contents: write
pull-requests: write
jobs:
sync:
runs-on: ubuntu-latest # pure git, no Xcode
env:
# CRITICAL (§4.2 BLOCKING) — do NOT use ${{ github.token }} here. By GitHub's documented
# recursion-prevention rule, a branch push / PR authored with the default GITHUB_TOKEN
# does NOT trigger other workflows — so guard.yml and ci_cd.yml's pull_request: checks
# would NEVER fire on the sync PR and the entire pre-merge gate would silently never run.
# SYNC_BOT_TOKEN MUST be a fork-owned credential that re-triggers workflows: a GitHub App
# installation token (preferred — least-privilege, short-lived) or a fork-owned PAT.
GH_TOKEN: ${{ secrets.SYNC_BOT_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
# Persist the fork-owned credential (NOT the default GITHUB_TOKEN) as the push
# credential, so the branch push below re-triggers guard.yml / ci_cd.yml PR checks.
token: ${{ secrets.SYNC_BOT_TOKEN }}
- name: Fail closed if SYNC_BOT_TOKEN is missing
run: |
# §4.2: without the workflow-re-triggering token the pre-merge gate is silently
# disarmed. Fail loudly here rather than open a check-less PR.
if [ -z "${SYNC_BOT_TOKEN:-}" ]; then
echo "::error::SYNC_BOT_TOKEN is not set. Refusing to open a check-less sync PR (§4.2)."
echo "Provision a GitHub App installation token (preferred) or fork-owned PAT as SYNC_BOT_TOKEN."
exit 1
fi
env:
SYNC_BOT_TOKEN: ${{ secrets.SYNC_BOT_TOKEN }}
- name: Add upstream + fetch tags
run: |
set -euo pipefail
git remote add upstream https://github.com/lwouis/alt-tab-macos.git || true
git fetch upstream --tags --force
- name: Verify sync labels exist (§4.2 — fail closed, never treat empty as "no open sync")
run: |
set -euo pipefail
# Guard 2 below and the §0.1 trip-wire query `sync` / `conflict` / `chokepoint-refresh`.
# `gh pr list --label <missing>` returns an EMPTY LIST, not an error, for a label that
# was never created — so on a fresh repo Guard 2 would silently no-op. Fail closed if a
# required label is absent; the bootstrap (§6.1) creates them with `gh label create`.
missing=0
for L in sync conflict chokepoint-refresh; do
if ! gh label list --json name --jq '.[].name' | grep -qxF "$L"; then
echo "::error::Required label '$L' is missing. Create it at bootstrap (§6.1): gh label create $L"
missing=1
fi
done
[ "$missing" -eq 0 ] || exit 1
- name: Select oldest upstream tag strictly newer than .fork-sync-state
id: tag
run: |
set -euo pipefail
# State-file-driven selection (§4.2): NOT "newest tag minus --is-ancestor".
last_synced="$(tr -d '[:space:]' < .fork-sync-state)"
if [ -z "$last_synced" ]; then
echo "::error::.fork-sync-state is empty; seed it with the last-merged upstream tag (e.g. v11.3.0)."
exit 1
fi
echo "Last synced (from .fork-sync-state): $last_synced"
# UPSTREAM-ONLY enumeration: `git tag -l 'v*'` would also match the FORK's OWN
# semantic-release tags (the fork cuts vX.Y.Z every sync — §4.3), so list tags from
# the upstream remote explicitly. Sort by version; keep only tags strictly newer than
# the recorded one; take the OLDEST of those (resolve-in-order — §4.2).
all_newer="$(git ls-remote --tags --refs upstream 'v*' \
| awk -F/ '{print $NF}' \
| sort -V \
| awk -v last="$last_synced" '
$0 == last { seen = 1; next }
seen { print }
')"
# Fallback for the case where last_synced is not itself present upstream: take every
# tag that sorts strictly after it.
if [ -z "$all_newer" ]; then
all_newer="$(git ls-remote --tags --refs upstream 'v*' \
| awk -F/ '{print $NF}' \
| sort -V \
| awk -v last="$last_synced" '{ if ($0 != last) print $0 }' \
| while read -r t; do
# keep only tags that sort AFTER last_synced
if [ "$(printf '%s\n%s\n' "$last" "$t" | sort -V | tail -1)" = "$t" ] && [ "$t" != "$last" ]; then
echo "$t"
fi
done)"
fi
next="$(printf '%s\n' "$all_newer" | sed '/^$/d' | head -1)"
if [ -z "$next" ]; then
echo "Up to date: no upstream tag newer than $last_synced."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Next tag to sync (oldest strictly newer): $next"
# Guard — a prior sync is still OPEN (resolve-in-order, §4.2): do NOT open a new PR
# while any sync/* PR is unmerged. A backlog pauses syncing until the human merges it;
# this is the never-drop-commits behavior. The liveness alert keys off the stalled flag.
# `gh pr list --label sync` fails closed because the label existence was verified above.
open_sync="$(gh pr list --state open --label sync --json number,headRefName,createdAt \
--jq 'sort_by(.createdAt) | .[0] // empty')"
if [ -n "$open_sync" ]; then
echo "A prior sync PR is still open (resolve-in-order): $open_sync"
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "stalled=true" >> "$GITHUB_OUTPUT" # liveness alert keys off this (§4.2)
exit 0
fi
echo "latest=$next" >> "$GITHUB_OUTPUT"
- name: Merge onto a sync branch (real merge commit, conventional message)
if: steps.tag.outputs.skip != 'true'
id: merge
run: |
set -euo pipefail
TAG='${{ steps.tag.outputs.latest }}'
git config user.name "fork-sync-bot"
git config user.email "fork-sync-bot@users.noreply.github.com"
git switch -c "sync/${TAG}"
# NO --no-commit: let git author a REAL merge commit with a CONVENTIONAL message so
# commitlint / @semantic-release/commit-analyzer stay happy (§4.3). On conflict, commit
# the conflicted tree (markers included, --no-verify) so the PR shows the markers for a
# human to resolve; Guard C (guard.yml + ci_cd.yml) then blocks it from ever shipping.
if git merge --no-ff -m "chore(sync): merge upstream ${TAG}" "${TAG}"; then
conflicts=false
else
git add -A
git commit --no-verify \
-m "chore(sync): merge upstream ${TAG} (CONFLICTS — resolve before merge)"
conflicts=true
fi
echo "conflicts=${conflicts}" >> "$GITHUB_OUTPUT"
# §0.1 trip-wire A — surface chokepoint churn as a per-sync signal. A nonzero count
# means an upstream change touched a de-paywall chokepoint file in this merge range
# (last-synced tag .. new tag); the PR is labeled `chokepoint-refresh` (below) so the
# trip-wire is a single query, not a memory exercise.
CHOKEPOINTS="src/pro/license/LicenseManager.swift src/pro/ProFeature.swift src/preferences/PreferenceDefinition.swift src/pro/scheduling/ProTransitionManager.swift src/pro/license/LicenseCookie.swift src/vendors/AppCenterCrashes.swift"
LAST_SYNCED="$(tr -d '[:space:]' < .fork-sync-state)"
churn="$(git rev-list --count "${LAST_SYNCED}..${TAG}" -- $CHOKEPOINTS 2>/dev/null || echo 0)"
# Also flag if the merge left a conflict in any chokepoint file (the conflicted commit
# is HEAD; compare against its first parent HEAD~1 = the pre-merge master tip).
chokepoint_conflict=0
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -qE 'LicenseManager\.swift|ProFeature\.swift|PreferenceDefinition\.swift|ProTransitionManager\.swift|LicenseCookie\.swift|AppCenterCrashes\.swift'; then
if [ "${conflicts:-false}" = "true" ]; then
chokepoint_conflict=1
fi
fi
echo "churn=${churn}" >> "$GITHUB_OUTPUT"
echo "chokepoint_conflict=${chokepoint_conflict}" >> "$GITHUB_OUTPUT"
git push origin "HEAD:sync/${TAG}"
- name: Open PR
if: steps.tag.outputs.skip != 'true'
run: |
set -euo pipefail
TAG='${{ steps.tag.outputs.latest }}'
CHURN='${{ steps.merge.outputs.churn }}'
CONFLICTS='${{ steps.merge.outputs.conflicts }}'
CHOKEPOINT_CONFLICT='${{ steps.merge.outputs.chokepoint_conflict }}'
# Label fail-closed (§4.2): conflict -> `sync,conflict`, else `sync`.
if [ "$CONFLICTS" = "true" ]; then
LABELS="sync,conflict"
else
LABELS="sync"
fi
BODY="Automated upstream sync to \`${TAG}\`.
Selected as the OLDEST upstream tag strictly newer than \`.fork-sync-state\` (§4.2).
Verify the guard.yml + ci_cd.yml PR checks are GREEN (anti-relock test + marker/conflict greps) before merge.
If checks did not appear, SYNC_BOT_TOKEN is mis-set to the default GITHUB_TOKEN — the gate is disarmed (§4.2).
Chokepoint-churn count for this merge range: ${CHURN} (nonzero => de-paywall chokepoint touched, see §0.1 trip-wire A).
NEVER close this PR to defer it — \`.fork-sync-state\` only advances on MERGE, so leaving it OPEN keeps this tag 'next'; closing it would silently skip the tag (§4.2 / §6.2)."
gh pr create \
--head "sync/${TAG}" \
--base master \
--title "sync: merge upstream ${TAG}" \
--body "$BODY" \
--label "$LABELS"
# §0.1 trip-wire A — auto-apply `chokepoint-refresh` from CI when a chokepoint file
# changed or conflicted, so the trip-wire tally never depends on a human remembering.
if [ "${CHURN:-0}" -gt 0 ] || [ "${CHOKEPOINT_CONFLICT:-0}" = "1" ]; then
gh pr edit "sync/${TAG}" --add-label chokepoint-refresh || true
fi
+1 -13
View File
@@ -1,25 +1,13 @@
/.idea/
xcuserdata/
/.vscode/
/buildServer.json
WorkspaceSettings.xcsettings
IDEWorkspaceChecks.plist
/node_modules/
/DerivedData/
/build/
/vendor/*/.build/
/vendor/*/build/
/vendor/*/.swiftpm/
/vendor/*/Package.resolved
.DS_Store
docs/_site/
codesign.conf
codesign.crt
codesign.key
codesign.p12
/.claude/*
!/.claude/skills/
/scripts/l10n/missing-translations.json
/ai/output/
# alt-tab-free [depaywall]: the Sparkle feed is published OUT OF TREE to the gh-pages branch by
# scripts/update_appcast.sh; the in-tree appcast.xml is removed (PLAN §3.3).
/appcast.xml
-6
View File
@@ -1,6 +0,0 @@
project: alt-tab-macos.xcodeproj
schemes:
- Release
index_exclude:
- vendor/**
- src/api-wrappers/private-apis/**
-10
View File
@@ -1,10 +0,0 @@
# rules: https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md
--swiftversion 5.8
--maxwidth 110
--nowrapoperators .,==,!=,<,>,>=,<=
--wrapternary before-operators
--disable andOperator
--disable wrapConditionalBodies
--disable wrapLoopBodies
--disable wrapMultilineStatementBraces
-10
View File
@@ -1,10 +0,0 @@
# **/*.swift: Matches all .swift files in the current directory and all its subdirectories.
# src/**/*.swift: Matches all .swift files in the src directory and all its subdirectories.
# !node_modules/**: Excludes all files in the node_modules directory and all its subdirectories.
Generated/**
**/PrivateApis.swift
# We will open a new branch to deal with code format issues at the next release.
**/*
+32
View File
@@ -0,0 +1,32 @@
branches:
only:
- master
os: osx
osx_image: xcode11.5
language: node_js
node_js:
- 10
addons:
ssh_known_hosts: github.com
homebrew:
packages:
- vitorgalvao/tiny-scripts/cask-repair
update: true
env:
global:
- IS_RELEASE=$(if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then echo true; fi)
- XCODE_BUILD_PATH="DerivedData/Build/Products/Release"
- VERSION_FILE="VERSION.txt"
- APP_NAME="$(awk -F ' = ' '/PRODUCT_NAME/ { print $2; }' < config/base.xcconfig)"
install: true # disable default install
script: scripts/travis.sh
deploy:
provider: releases
token:
secure: wcAMT2rbXhhdk6lsc6EG9VZZkSW0BjOwjYSyXzHjKrgiXsQcZAzAsOkuisj8EfeYN9/uhpa8XX7f5KsdkE5reMqVGGWqdmJU4NnmK5Aew0LmBbzFHfffliQecVCzMmGYPmhPcDxAjidYxgCMrMbvQFaElc0WXdIsQNcaNieSkEgZw6TwGsTxgHXABD2e575K3SN/PPcq7Nx5mRZkURI7YLmo2KfsD2OPR4IcEXFpJRFbPcAZIpKaHUIC2VzpayJA93pBe+siwU150p04PzfSs/nTH/nUO9m24SUi6kAIzVN77cExvX+JDnQoVLz0V45L0iJjkKDTVb07rVOsfm6Pcuaaj2ZVXlpujSb7B5LDUIspUn/NnqMLOtzbSTuaGcx2u+zCcC0b17KooznilQ/sFq3YpeNdmjL4VL5fsw1hdpjcP6S9DqZ90TJZqy6xBQP5NubuJukH//BRilhWGfnq5rOFUlqrN8aApJ7gYry0K2rB8rVWWvA9cU+zX351iy0EERICEU6q3rQwq8gFUbdNATPJJgNg7n0PaNXSQTTF9VluZTRVUtMrpA0hWe7dn+Tvrvhrnhmqvccw3Qfw/i3SECIDHHtsjwLqHCx5gkz/xUdf7+t3k6x+r6Veua0Z3jvyVhdHprU47VTSUhM/n87EiKyNRuKa9IpA18eSlZ59exU=
file_glob: true
file: "$XCODE_BUILD_PATH/*.zip"
skip_cleanup: true
on:
repo: lwouis/alt-tab-macos
after_deploy: scripts/update_homebrew_cask.sh
-14
View File
@@ -1,14 +0,0 @@
# macOS development
- Don't use xcode directly to develop
- Use pure swift 5.8 code to make the app. No interface builder. No SwiftUI.
- Aim for compact code. Within methods, don't have groups of statements separated with newlines. No inline comments for simple code. Instead, split statements into sub-methods.
- Use guard closes as much as possible to separate the happy-path under them
- Organize source files into folders. Folders should group files that change together, at the same pace (e.g. one feature)
- Favor low latency and responsiveness. Reuse objects, avoid wasting memory or I/O.
# Workflow
- Copy commands from ai/build.sh and run them, to confirm compilation works after you're done with implementing a change
# License / Keychain invariant
- The app's Developer ID, TeamID, and bundle ID must remain stable across builds. Keychain items are tied to the code signature; changing any of these orphans every user's stored license key and forces mass re-activation. If a rotation is unavoidable, plan a migration first (e.g., a backup-restore handler, or `kSecAttrAccessGroup` with a stable group identifier).
- Do not introduce legacy `SecKeychain*` API or `kSecAccessControl` (biometric/PIN gating) into license code — both can trigger Keychain password prompts, which is bad UX for license activation.
-1
View File
@@ -1 +0,0 @@
@include ./AGENTS.md
+30 -58
View File
@@ -2,83 +2,55 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- ===== Bundle identity (required by macOS / App Store) ===== -->
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>ATSApplicationFontsPath</key>
<string></string>
<key>AppCenterApplicationForwarderEnabled</key>
<string>0</string>
<key>AppCenterSecret</key>
<string>#APPCENTER_SECRET#</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string>app.icns</string>
<key>NSHumanReadableCopyright</key>
<string>GPL-3.0 licence</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>#VERSION#</string>
<key>CFBundleVersion</key>
<string>#VERSION#</string>
<key>FeedbackToken</key>
<string>#FEEDBACK_TOKEN#</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<!-- ===== Versioning (substituted from xcconfig; CI overrides via config/local.xcconfig) ===== -->
<key>CFBundleShortVersionString</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<!-- ===== Launch behavior (read by LaunchServices before app starts) ===== -->
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>GPL-3.0 licence</string>
<key>NSPrincipalClass</key>
<string>AppCenterApplication</string>
<key>NSSupportsAutomaticTermination</key>
<string>false</string>
<key>NSSupportsSuddenTermination</key>
<string>false</string>
<key>ATSApplicationFontsPath</key>
<string></string>
<!-- ===== URL scheme registration (LaunchServices dispatches $(PRODUCT_BUNDLE_IDENTIFIER):// to us) ===== -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleURLSchemes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<!-- ===== Sparkle (auto-update framework — reads these directly from the bundle) ===== -->
<key>SUPublicEDKey</key>
<!-- Fork EdDSA public key for Sparkle update verification. The matching private key
is held as CI secret SPARKLE_ED_PRIVATE_KEY (see docs/EXECUTION-STATUS.md §3.3).
Auto-checks stay off (below) until DOMAIN serves a real appcast host. -->
<string>1YfoCJDk/T8GQx1Gpf3O4ApFgDkBiN6oe/z/Kfnxs/M=</string>
<key>SUEnableAutomaticChecks</key>
<!-- alt-tab-free: enabled 2026-06-16 — DOMAIN serves the appcast on GitHub Pages (gh-pages). -->
<string>true</string>
<key>SUEnableJavaScript</key>
<string>true</string>
<key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/lwouis/alt-tab-macos/master/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>2e9SQOBoaKElchSa/4QDli/nvYkyuDNfynfzBF6vJK4=</string>
<key>SUScheduledCheckInterval</key>
<string>604800</string>
<!-- ===== AppCenter (crash reporter — read by AppCenterApplication superclass at init) ===== -->
<key>AppCenterApplicationForwarderEnabled</key>
<string>0</string>
<!-- ===== Build-time secrets (substituted from xcconfig; override per-dev via config/local.xcconfig) ===== -->
<key>AppCenterSecret</key>
<string>$(APPCENTER_SECRET)</string>
<key>Domain</key>
<string>$(DOMAIN)</string>
<key>ApiDomain</key>
<string>$(API_DOMAIN)</string>
</dict>
</plist>
-34
View File
@@ -1,34 +0,0 @@
# NOTICE — Statement of Changes (GPL-3.0 §5(a))
This file carries the prominent notice, required by GNU GPL v3.0 §5(a), that this work
has been modified, and gives the relevant date. It is a **living document**: whenever a
sync from upstream changes this fork's modification surface, update the change list and
the date below (see `docs/PLAN-maintained-fork.md` §6.2).
**Forked from:** [lwouis/alt-tab-macos](https://github.com/lwouis/alt-tab-macos) v11.3.0,
© Louis Pontoise, licensed GPL-3.0.
**Modified by:** the CommandTabFree fork. This fork neutralizes the AltTab Pro paywall so that
all features are free.
**Date of modification:** 2026-06-10.
**License:** CommandTabFree is released under the **GNU General Public License v3.0**, the same
license as upstream. The full license text is in [`LICENCE.md`](LICENCE.md), which is kept
byte-for-byte identical to upstream and is never edited.
## Changes made in this fork
- **`LicenseManager.computeState`** (`src/pro/license/LicenseManager.swift`) — forced to
return `.pro`, opening every Pro feature gate. This is the single live license-state
producer, so this one change unlocks all paywalled features.
- **`LicenseCookie.syncLicenseCookie`** (`src/pro/license/LicenseCookie.swift`) — made a
no-op, so no license cookie is written to the upstream domain.
- **`AppCenterCrash.init`** (`src/vendors/AppCenterCrashes.swift`) — telemetry disabled;
AppCenter is never started.
- **`App.handleCustomUrl`** (`src/App.swift`) — the `activate` deep-link handler is
disabled, so a crafted `…://activate?license_key=…` URL can no longer POST the machine
hardware fingerprint to a license backend.
The upstream copyright, contributor credits (`docs/contributors.md`), and third-party
acknowledgments (`docs/acknowledgments.md`) are retained unchanged.
+2 -8
View File
@@ -6,12 +6,6 @@ verify_ssl = true
[dev-packages]
[packages]
fonttools = "==4.8.1"
googletrans = "==3.0.0"
fonttools = "*"
[requires]
python_version = "3.6"
[scripts]
font2png = "scripts/convert_font_to_png.sh"
translate = "python scripts/l10n/google_translate_all_terms_locally.py resources/l10n --retry_translate"
[scripts]
Generated
+2 -102
View File
@@ -1,12 +1,10 @@
{
"_meta": {
"hash": {
"sha256": "3646221352b85667ac21f1b528dc8b29f56ff1e24cb97e7eaba79b5040c78e21"
"sha256": "8f11c81dff5e32871b7cb2314acb76fd1fb4a06fdb403a65bc6749fe3770eda6"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"requires": {},
"sources": [
{
"name": "pypi",
@@ -16,111 +14,13 @@
]
},
"default": {
"certifi": {
"hashes": [
"sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407",
"sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"
],
"markers": "python_version >= '3.7'",
"version": "==2025.8.3"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"fonttools": {
"hashes": [
"sha256:596c0e8399c650dbf1048b7ca6fc7b12bd79a7db0981f3b87743de8c2b7512de",
"sha256:6d241d319ea9eae4c4ee27cbd1db8656dbd496dd9aa884401debfab3a4c7d82b"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==4.8.1"
},
"googletrans": {
"hashes": [
"sha256:44caeea42d91ff6ead5c2d49db2b88de66c45c2fe874c8ec03eb9b4ceb3a533d"
],
"index": "pypi",
"version": "==3.0.0"
},
"h11": {
"hashes": [
"sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1",
"sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"
],
"version": "==0.9.0"
},
"h2": {
"hashes": [
"sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5",
"sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"
],
"version": "==3.2.0"
},
"hpack": {
"hashes": [
"sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89",
"sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"
],
"version": "==3.0.0"
},
"hstspreload": {
"hashes": [
"sha256:02bd5b0885b62f0d5239ca6c6876ffcad8d9c08dc97cee54292598141a16e9dc",
"sha256:346552a807b3a1762376de8ecce097544e7fcd64fb64231b4652da52f86fa6f1"
],
"markers": "python_version >= '3.6'",
"version": "==2025.1.1"
},
"httpcore": {
"hashes": [
"sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0",
"sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9"
],
"markers": "python_version >= '3.6'",
"version": "==0.9.1"
},
"httpx": {
"hashes": [
"sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335",
"sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae"
],
"markers": "python_version >= '3.6'",
"version": "==0.13.3"
},
"hyperframe": {
"hashes": [
"sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40",
"sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"
],
"version": "==5.2.0"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10"
},
"rfc3986": {
"hashes": [
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
],
"version": "==1.5.0"
},
"sniffio": {
"hashes": [
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
],
"markers": "python_version >= '3.7'",
"version": "==1.3.1"
}
},
"develop": {}
+10
View File
@@ -0,0 +1,10 @@
platform :osx, '10.12'
target 'alt-tab-macos' do
use_frameworks!
pod 'LetsMove', :git => 'https://github.com/lwouis/LetsMove.git', :commit => '7abf4daed1a25218f2b52f2dfd190aee5a50071c'
pod 'Sparkle', :podspec => 'https://raw.githubusercontent.com/lwouis/Sparkle/master/Sparkle.podspec'
pod 'ShortcutRecorder', :git => 'https://github.com/Kentzo/ShortcutRecorder.git', :commit => 'f718220854575bad86fa89dd2bb3decd0b14b2b0'
pod 'SwiftyMarkdown', '1.1.0'
pod 'AppCenter/Crashes', '3.3.1'
end
+49
View File
@@ -0,0 +1,49 @@
PODS:
- AppCenter/Core (3.3.1)
- AppCenter/Crashes (3.3.1):
- AppCenter/Core
- LetsMove (1.24)
- ShortcutRecorder (3.1)
- Sparkle (1.23.1)
- SwiftyMarkdown (1.1.0)
DEPENDENCIES:
- AppCenter/Crashes (= 3.3.1)
- LetsMove (from `https://github.com/lwouis/LetsMove.git`, commit `7abf4daed1a25218f2b52f2dfd190aee5a50071c`)
- ShortcutRecorder (from `https://github.com/Kentzo/ShortcutRecorder.git`, commit `f718220854575bad86fa89dd2bb3decd0b14b2b0`)
- Sparkle (from `https://raw.githubusercontent.com/lwouis/Sparkle/master/Sparkle.podspec`)
- SwiftyMarkdown (= 1.1.0)
SPEC REPOS:
trunk:
- AppCenter
- SwiftyMarkdown
EXTERNAL SOURCES:
LetsMove:
:commit: 7abf4daed1a25218f2b52f2dfd190aee5a50071c
:git: https://github.com/lwouis/LetsMove.git
ShortcutRecorder:
:commit: f718220854575bad86fa89dd2bb3decd0b14b2b0
:git: https://github.com/Kentzo/ShortcutRecorder.git
Sparkle:
:podspec: https://raw.githubusercontent.com/lwouis/Sparkle/master/Sparkle.podspec
CHECKOUT OPTIONS:
LetsMove:
:commit: 7abf4daed1a25218f2b52f2dfd190aee5a50071c
:git: https://github.com/lwouis/LetsMove.git
ShortcutRecorder:
:commit: f718220854575bad86fa89dd2bb3decd0b14b2b0
:git: https://github.com/Kentzo/ShortcutRecorder.git
SPEC CHECKSUMS:
AppCenter: 76ecf7e959a68e36b0fd314e8dc5897660214423
LetsMove: fefe56bc7bc7fb7d37049e28a14f297961229fc5
ShortcutRecorder: fdf620aca34101b0cba3b10fca815e0459254189
Sparkle: ea073ba9c69cee3ebaab8685ba1e732e14d249f7
SwiftyMarkdown: 033abc9dad9a37903f81aec5188cade436f40520
PODFILE CHECKSUM: 78df0a34910c4c031a65ac3b58dc004c21b552d0
COCOAPODS: 1.9.1
+25
View File
@@ -0,0 +1,25 @@
Visual Studio App Center SDK for Apple platforms
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
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:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
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.
@@ -0,0 +1 @@
Versions/Current/AppCenter
@@ -0,0 +1 @@
Versions/Current/PrivateHeaders
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#import <Foundation/Foundation.h>
#import "MSAbstractLog.h"
#import "MSAppCenter.h"
#import "MSAppCenterErrors.h"
#import "MSChannelGroupProtocol.h"
#import "MSChannelProtocol.h"
#import "MSConstants.h"
#import "MSCustomProperties.h"
#import "MSDevice.h"
#import "MSEnable.h"
#import "MSLog.h"
#import "MSLogWithProperties.h"
#import "MSLogger.h"
#import "MSService.h"
#import "MSServiceAbstract.h"
#import "MSWrapperLogger.h"
#import "MSWrapperSdk.h"
@@ -1,11 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_ABSTRACT_LOG_H
#define MS_ABSTRACT_LOG_H
#import <Foundation/Foundation.h>
#import "MSACDBStorage.h"
#import "MSACStorage.h"
@interface MSACLogDBStorage : MSACDBStorage <MSACStorage>
@interface MSAbstractLog : NSObject
@end
#endif
@@ -3,26 +3,18 @@
#import <Foundation/Foundation.h>
#ifndef MSAC_APP_CENTER
#define MSAC_APP_CENTER
#import "MSConstants.h"
#if __has_include(<AppCenter/MSACConstants.h>)
#import <AppCenter/MSACConstants.h>
#else
#import "MSACConstants.h"
#endif
@class MSACWrapperSdk;
@class MSWrapperSdk;
#if !TARGET_OS_TV
@class MSACCustomProperties;
@class MSCustomProperties;
#endif
NS_SWIFT_NAME(AppCenter)
@interface MSACAppCenter : NSObject
@interface MSAppCenter : NSObject
/**
* Returns the singleton instance of MSACAppCenter.
* Returns the singleton instance of MSAppCenter.
*/
+ (instancetype)sharedInstance;
@@ -33,7 +25,7 @@ NS_SWIFT_NAME(AppCenter)
*
* @discussion This may be called only once per application process lifetime.
*/
+ (void)configureWithAppSecret:(NSString *)appSecret NS_SWIFT_NAME(configure(withAppSecret:));
+ (void)configureWithAppSecret:(NSString *)appSecret;
/**
* Configure the SDK.
@@ -50,7 +42,7 @@ NS_SWIFT_NAME(AppCenter)
*
* @discussion This may be called only once per application process lifetime.
*/
+ (void)start:(NSString *)appSecret withServices:(NSArray<Class> *)services NS_SWIFT_NAME(start(withAppSecret:services:));
+ (void)start:(NSString *)appSecret withServices:(NSArray<Class> *)services;
/**
* Start the SDK with an array of services.
@@ -59,7 +51,7 @@ NS_SWIFT_NAME(AppCenter)
*
* @discussion This may be called only once per application process lifetime.
*/
+ (void)startWithServices:(NSArray<Class> *)services NS_SWIFT_NAME(start(services:));
+ (void)startWithServices:(NSArray<Class> *)services;
/**
* Start a service.
@@ -76,53 +68,76 @@ NS_SWIFT_NAME(AppCenter)
*
* @param services Array of services to start.
*/
+ (void)startFromLibraryWithServices:(NSArray<Class> *)services NS_SWIFT_NAME(startFromLibrary(services:));
+ (void)startFromLibraryWithServices:(NSArray<Class> *)services;
/**
* The flag indicates whether the SDK has already been configured or not.
*/
@property(class, atomic, readonly, getter=isConfigured) BOOL configured;
/**
* The flag indicates whether app is running in App Center Test Cloud.
*/
@property(class, atomic, readonly, getter=isRunningInAppCenterTestCloud) BOOL runningInAppCenterTestCloud;
/**
* The flag indicates whether or not the SDK was enabled as a whole
* Check whether the SDK has already been configured or not.
*
* The state is persisted in the device's storage across application launches.
* @return YES if configured, NO otherwise.
*/
@property(class, nonatomic, getter=isEnabled, setter=setEnabled:) BOOL enabled NS_SWIFT_NAME(enabled);
+ (BOOL)isConfigured;
/**
* Flag indicating whether SDK can send network requests.
* Check whether app is running in App Center Test Cloud.
*
* @return true if running in App Center Test Cloud, false otherwise.
*/
+ (BOOL)isRunningInAppCenterTestCloud;
/**
* Change the base URL (schema + authority + port only) used to communicate with the backend.
*
* @param logUrl Base URL to use for backend communication.
*/
+ (void)setLogUrl:(NSString *)logUrl;
/**
* Enable or disable the SDK as a whole. In addition to AppCenter resources, it will also enable or disable all registered services.
* The state is persisted in the device's storage across application launches.
*
* @param isEnabled YES to enable, NO to disable.
*
* @see isEnabled
*/
@property(class, nonatomic, getter=isNetworkRequestsAllowed, setter=setNetworkRequestsAllowed:)
BOOL networkRequestsAllowed NS_SWIFT_NAME(networkRequestsAllowed);
+ (void)setEnabled:(BOOL)isEnabled;
/**
* The SDK's log level.
* Check whether the SDK is enabled or not as a whole.
*
* @return YES if enabled, NO otherwise.
*
* @see setEnabled:
*/
@property(class, nonatomic) MSACLogLevel logLevel;
+ (BOOL)isEnabled;
/**
* Base URL to use for backend communication.
* Get log level.
*
* @return Log level.
*/
@property(class, nonatomic, strong) NSString *logUrl;
+ (MSLogLevel)logLevel;
/**
* Set log handler.
* Set log level.
*
* @param logLevel The log level.
*/
@property(class, nonatomic) MSACLogHandler logHandler;
+ (void)setLogLevel:(MSLogLevel)logLevel;
/**
* Set log level handler.
*
* @param logHandler Handler.
*/
+ (void)setLogHandler:(MSLogHandler)logHandler;
/**
* Set wrapper SDK information to use when building device properties. This is intended in case you are building a SDK that uses the App
* Center SDK under the hood, e.g. our Xamarin SDK or ReactNative SDk.
*
* @param wrapperSdk Wrapper SDK information.
*/
@property(class, nonatomic, strong) MSACWrapperSdk *wrapperSdk;
+ (void)setWrapperSdk:(MSWrapperSdk *)wrapperSdk;
#if !TARGET_OS_TV
/**
@@ -130,12 +145,14 @@ NS_SWIFT_NAME(AppCenter)
*
* @param customProperties Custom properties object.
*/
+ (void)setCustomProperties:(MSACCustomProperties *)customProperties;
+ (void)setCustomProperties:(MSCustomProperties *)customProperties;
#endif
/**
* Check whether the application delegate forwarder is enabled or not.
*
* @return YES if enabled, NO otherwise.
*
* @discussion The application delegate forwarder forwards messages that target your application delegate methods via swizzling to the SDK.
* It simplifies the SDK integration but may not be suitable to any situations. For
* instance it should be disabled if you or one of your third party SDK is doing message forwarding on the application delegate. Message
@@ -143,26 +160,29 @@ NS_SWIFT_NAME(AppCenter)
* To disable the application delegate forwarder just add the `AppCenterAppDelegateForwarderEnabled` tag to your Info .plist file and set it
* to `0`. Then you will have to forward any application delegate needed by the SDK manually.
*/
@property(class, readonly, nonatomic, getter=isAppDelegateForwarderEnabled) BOOL appDelegateForwarderEnabled;
+ (BOOL)isAppDelegateForwarderEnabled;
/**
* Unique installation identifier.
* Get unique installation identifier.
*
* @return Unique installation identifier.
*/
@property(class, readonly, nonatomic) NSUUID *installId;
+ (NSUUID *)installId;
/**
* Detect if a debugger is attached to the app process. This is only invoked once on app startup and can not detect
* if the debugger is being attached during runtime!
*
* @return BOOL if the debugger is attached.
*/
@property(class, readonly, nonatomic, getter=isDebuggerAttached) BOOL debuggerAttached;
+ (BOOL)isDebuggerAttached;
/**
* Current version of AppCenter SDK.
* Get the current version of AppCenter SDK.
*
* @return The current version of AppCenter SDK.
*/
@property(class, readonly, nonatomic) NSString *sdkVersion;
+ (NSString *)sdkVersion;
/**
* Set the maximum size of the internal storage. This method must be called before App Center is started. This method is only intended for
@@ -182,22 +202,22 @@ NS_SWIFT_NAME(AppCenter)
/**
* Set the user identifier.
*
* @param userId User identifier.
*
* @discussion Set the user identifier for logs sent for the default target token when the secret passed in @c
* MSACAppCenter:start:withServices: contains "target={targetToken}".
* MSAppCenter:start:withServices: contains "target={targetToken}".
*
* For App Center backend the user identifier maximum length is 256 characters.
*
* AppCenter must be configured or started before this API can be used.
*/
@property(class, nonatomic, strong) NSString *userId;
+ (void)setUserId:(NSString *)userId;
/**
* Set country code to use when building device properties.
*
* @see https://www.iso.org/obp/ui/#search for more information.
* @param countryCode The two-letter ISO country code. @see https://www.iso.org/obp/ui/#search for more information.
*/
@property(class, nonatomic, strong) NSString *countryCode;
+ (void)setCountryCode:(NSString *)countryCode;
@end
#endif
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_APP_CENTER_ERRORS_H
#define MS_APP_CENTER_ERRORS_H
#import <Foundation/Foundation.h>
#define MS_APP_CENTER_BASE_DOMAIN @"com.Microsoft.AppCenter."
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Domain
static NSString *const kMSACErrorDomain = MS_APP_CENTER_BASE_DOMAIN @"ErrorDomain";
#pragma mark - General
// Error codes.
NS_ENUM(NSInteger){MSACLogInvalidContainerErrorCode = 1, MSACCanceledErrorCode = 2, MSACDisabledErrorCode = 3};
// Error descriptions.
static NSString const *kMSACLogInvalidContainerErrorDesc = @"Invalid log container.";
static NSString const *kMSACCanceledErrorDesc = @"The operation was canceled.";
static NSString const *kMSACDisabledErrorDesc = @"The service is disabled.";
#pragma mark - Connection
// Error codes.
NS_ENUM(NSInteger){MSACConnectionPausedErrorCode = 100, MSACConnectionHttpErrorCode = 101};
// Error descriptions.
static NSString const *kMSACConnectionHttpErrorDesc = @"An HTTP error occured.";
static NSString const *kMSACConnectionPausedErrorDesc = @"Canceled, connection paused with log deletion.";
// Error user info keys.
static NSString const *kMSACConnectionHttpCodeErrorKey = @"MSACConnectionHttpCode";
NS_ASSUME_NONNULL_END
#endif
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_CHANNEL_GROUP_PROTOCOL_H
#define MS_CHANNEL_GROUP_PROTOCOL_H
#import <Foundation/Foundation.h>
#import "MSChannelProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@class MSChannelUnitConfiguration;
@protocol MSIngestionProtocol;
@protocol MSChannelUnitProtocol;
/**
* `MSChannelGroupProtocol` represents a kind of channel that contains constituent MSChannelUnit objects. When an operation from the
* `MSChannelProtocol` is performed on the group, that operation should be propagated to its constituent MSChannelUnit objects.
*/
@protocol MSChannelGroupProtocol <MSChannelProtocol>
/**
* Initialize a channel unit with the given configuration.
*
* @param configuration channel configuration.
*
* @return The added `MSChannelUnitProtocol`. Use this object to enqueue logs.
*/
- (id<MSChannelUnitProtocol>)addChannelUnitWithConfiguration:(MSChannelUnitConfiguration *)configuration;
/**
* Initialize a channel unit with the given configuration.
*
* @param configuration channel configuration.
* @param ingestion The alternative ingestion object
*
* @return The added `MSChannelUnitProtocol`. Use this object to enqueue logs.
*/
- (id<MSChannelUnitProtocol>)addChannelUnitWithConfiguration:(MSChannelUnitConfiguration *)configuration
withIngestion:(nullable id<MSIngestionProtocol>)ingestion;
/**
* Change the base URL (schema + authority + port only) used to communicate with the backend.
*
* @param logUrl base URL to use for backend communication.
*/
- (void)setLogUrl:(NSString *)logUrl;
/**
* Set the app secret.
*
* @param appSecret The app secret.
*/
- (void)setAppSecret:(NSString *)appSecret;
/**
* Set the maximum size of the internal storage. This method must be called before App Center is started.
*
* @discussion The default maximum database size is 10485760 bytes (10 MiB).
*
* @param sizeInBytes Maximum size of the internal storage in bytes. This will be rounded up to the nearest multiple of a SQLite page size
* (default is 4096 bytes). Values below 24576 bytes (24 KiB) will be ignored.
* @param completionHandler Callback that is invoked when the database size has been set. The `BOOL` parameter is `YES` if changing the size
* is successful, and `NO` otherwise.
*/
- (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(nullable void (^)(BOOL))completionHandler;
/**
* Return a channel unit instance for the given groupId.
*
* @param groupId The group ID for a channel unit.
*
* @return A channel unit instance or `nil`.
*/
- (nullable id<MSChannelUnitProtocol>)channelUnitForGroupId:(NSString *)groupId;
@end
NS_ASSUME_NONNULL_END
#endif
@@ -1,41 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MSAC_CHANNEL_PROTOCOL_H
#define MSAC_CHANNEL_PROTOCOL_H
#ifndef MS_CHANNEL_PROTOCOL_H
#define MS_CHANNEL_PROTOCOL_H
#import <Foundation/Foundation.h>
#if __has_include(<AppCenter/MSACEnable.h>)
#import <AppCenter/MSACEnable.h>
#else
#import "MSACEnable.h"
#endif
#import "MSEnable.h"
NS_ASSUME_NONNULL_BEGIN
@protocol MSACChannelDelegate;
@protocol MSChannelDelegate;
/**
* `MSACChannelProtocol` contains the essential operations of a channel. Channels are broadly responsible for enqueuing logs to be sent to
* the backend and/or stored on disk.
* `MSChannelProtocol` contains the essential operations of a channel. Channels are broadly responsible for enqueuing logs to be sent to the
* backend and/or stored on disk.
*/
NS_SWIFT_NAME(ChannelProtocol)
@protocol MSACChannelProtocol <NSObject, MSACEnable>
@protocol MSChannelProtocol <NSObject, MSEnable>
/**
* Add delegate.
*
* @param delegate delegate.
*/
- (void)addDelegate:(id<MSACChannelDelegate>)delegate;
- (void)addDelegate:(id<MSChannelDelegate>)delegate;
/**
* Remove delegate.
*
* @param delegate delegate.
*/
- (void)removeDelegate:(id<MSACChannelDelegate>)delegate;
- (void)removeDelegate:(id<MSChannelDelegate>)delegate;
/**
* Pause operations, logs will be stored but not sent.
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#import <Foundation/Foundation.h>
/**
* Log Levels
*/
typedef NS_ENUM(NSUInteger, MSLogLevel) {
/**
* Logging will be very chatty
*/
MSLogLevelVerbose = 2,
/**
* Debug information will be logged
*/
MSLogLevelDebug = 3,
/**
* Information will be logged
*/
MSLogLevelInfo = 4,
/**
* Errors and warnings will be logged
*/
MSLogLevelWarning = 5,
/**
* Errors will be logged
*/
MSLogLevelError = 6,
/**
* Only critical errors will be logged
*/
MSLogLevelAssert = 7,
/**
* Logging is disabled
*/
MSLogLevelNone = 99
};
typedef NSString * (^MSLogMessageProvider)(void);
typedef void (^MSLogHandler)(MSLogMessageProvider messageProvider, MSLogLevel logLevel, NSString *tag, const char *file,
const char *function, uint line);
/**
* Channel priorities, check the kMSPriorityCount if you add a new value.
* The order matters here! Values NEED to range from low priority to high priority.
*/
typedef NS_ENUM(NSInteger, MSPriority) { MSPriorityBackground, MSPriorityDefault, MSPriorityHigh };
static short const kMSPriorityCount = MSPriorityHigh + 1;
/**
* The priority by which the modules are initialized.
* MSPriorityMax is reserved for only 1 module and this needs to be Crashes.
* Crashes needs to be initialized first to catch crashes in our other SDK Modules (which will hopefully never happen) and to avoid losing
* any log at crash time.
*/
typedef NS_ENUM(NSInteger, MSInitializationPriority) {
MSInitializationPriorityDefault = 500,
MSInitializationPriorityHigh = 750,
MSInitializationPriorityMax = 999
};
/**
* Enum with the different HTTP status codes.
*/
typedef NS_ENUM(NSInteger, MSHTTPCodesNo) {
// Invalid
MSHTTPCodesNo0XXInvalidUnknown = 0,
// Informational
MSHTTPCodesNo1XXInformationalUnknown = 1,
MSHTTPCodesNo100Continue = 100,
MSHTTPCodesNo101SwitchingProtocols = 101,
MSHTTPCodesNo102Processing = 102,
// Success
MSHTTPCodesNo2XXSuccessUnknown = 2,
MSHTTPCodesNo200OK = 200,
MSHTTPCodesNo201Created = 201,
MSHTTPCodesNo202Accepted = 202,
MSHTTPCodesNo203NonAuthoritativeInformation = 203,
MSHTTPCodesNo204NoContent = 204,
MSHTTPCodesNo205ResetContent = 205,
MSHTTPCodesNo206PartialContent = 206,
MSHTTPCodesNo207MultiStatus = 207,
MSHTTPCodesNo208AlreadyReported = 208,
MSHTTPCodesNo209IMUsed = 209,
// Redirection
MSHTTPCodesNo3XXSuccessUnknown = 3,
MSHTTPCodesNo300MultipleChoices = 300,
MSHTTPCodesNo301MovedPermanently = 301,
MSHTTPCodesNo302Found = 302,
MSHTTPCodesNo303SeeOther = 303,
MSHTTPCodesNo304NotModified = 304,
MSHTTPCodesNo305UseProxy = 305,
MSHTTPCodesNo306SwitchProxy = 306,
MSHTTPCodesNo307TemporaryRedirect = 307,
MSHTTPCodesNo308PermanentRedirect = 308,
// Client error
MSHTTPCodesNo4XXSuccessUnknown = 4,
MSHTTPCodesNo400BadRequest = 400,
MSHTTPCodesNo401Unauthorised = 401,
MSHTTPCodesNo402PaymentRequired = 402,
MSHTTPCodesNo403Forbidden = 403,
MSHTTPCodesNo404NotFound = 404,
MSHTTPCodesNo405MethodNotAllowed = 405,
MSHTTPCodesNo406NotAcceptable = 406,
MSHTTPCodesNo407ProxyAuthenticationRequired = 407,
MSHTTPCodesNo408RequestTimeout = 408,
MSHTTPCodesNo409Conflict = 409,
MSHTTPCodesNo410Gone = 410,
MSHTTPCodesNo411LengthRequired = 411,
MSHTTPCodesNo412PreconditionFailed = 412,
MSHTTPCodesNo413RequestEntityTooLarge = 413,
MSHTTPCodesNo414RequestURITooLong = 414,
MSHTTPCodesNo415UnsupportedMediaType = 415,
MSHTTPCodesNo416RequestedRangeNotSatisfiable = 416,
MSHTTPCodesNo417ExpectationFailed = 417,
MSHTTPCodesNo418IamATeapot = 418,
MSHTTPCodesNo419AuthenticationTimeout = 419,
MSHTTPCodesNo420MethodFailureSpringFramework = 420,
MSHTTPCodesNo420EnhanceYourCalmTwitter = 4200,
MSHTTPCodesNo422UnprocessableEntity = 422,
MSHTTPCodesNo423Locked = 423,
MSHTTPCodesNo424FailedDependency = 424,
MSHTTPCodesNo424MethodFailureWebDaw = 4240,
MSHTTPCodesNo425UnorderedCollection = 425,
MSHTTPCodesNo426UpgradeRequired = 426,
MSHTTPCodesNo428PreconditionRequired = 428,
MSHTTPCodesNo429TooManyRequests = 429,
MSHTTPCodesNo431RequestHeaderFieldsTooLarge = 431,
MSHTTPCodesNo444NoResponseNginx = 444,
MSHTTPCodesNo449RetryWithMicrosoft = 449,
MSHTTPCodesNo450BlockedByWindowsParentalControls = 450,
MSHTTPCodesNo451RedirectMicrosoft = 451,
MSHTTPCodesNo451UnavailableForLegalReasons = 4510,
MSHTTPCodesNo494RequestHeaderTooLargeNginx = 494,
MSHTTPCodesNo495CertErrorNginx = 495,
MSHTTPCodesNo496NoCertNginx = 496,
MSHTTPCodesNo497HTTPToHTTPSNginx = 497,
MSHTTPCodesNo499ClientClosedRequestNginx = 499,
// Server error
MSHTTPCodesNo5XXSuccessUnknown = 5,
MSHTTPCodesNo500InternalServerError = 500,
MSHTTPCodesNo501NotImplemented = 501,
MSHTTPCodesNo502BadGateway = 502,
MSHTTPCodesNo503ServiceUnavailable = 503,
MSHTTPCodesNo504GatewayTimeout = 504,
MSHTTPCodesNo505HTTPVersionNotSupported = 505,
MSHTTPCodesNo506VariantAlsoNegotiates = 506,
MSHTTPCodesNo507InsufficientStorage = 507,
MSHTTPCodesNo508LoopDetected = 508,
MSHTTPCodesNo509BandwidthLimitExceeded = 509,
MSHTTPCodesNo510NotExtended = 510,
MSHTTPCodesNo511NetworkAuthenticationRequired = 511,
MSHTTPCodesNo522ConnectionTimedOut = 522,
MSHTTPCodesNo598NetworkReadTimeoutErrorUnknown = 598,
MSHTTPCodesNo599NetworkConnectTimeoutErrorUnknown = 599
};
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MSAC_CUSTOM_PROPERTIES_H
#define MSAC_CUSTOM_PROPERTIES_H
#ifndef MS_CUSTOM_PROPERTIES_H
#define MS_CUSTOM_PROPERTIES_H
#import <Foundation/Foundation.h>
@@ -10,8 +10,7 @@
* Custom properties builder.
* Collects multiple properties to send in one log.
*/
NS_SWIFT_NAME(CustomProperties)
@interface MSACCustomProperties : NSObject
@interface MSCustomProperties : NSObject
/**
* Set the specified property value with the specified key.
@@ -22,7 +21,7 @@ NS_SWIFT_NAME(CustomProperties)
*
* @return This instance.
*/
- (instancetype)setString:(NSString *)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
- (instancetype)setString:(NSString *)value forKey:(NSString *)key;
/**
* Set the specified property value with the specified key.
@@ -33,7 +32,7 @@ NS_SWIFT_NAME(CustomProperties)
*
* @return This instance.
*/
- (instancetype)setNumber:(NSNumber *)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
- (instancetype)setNumber:(NSNumber *)value forKey:(NSString *)key;
/**
* Set the specified property value with the specified key.
@@ -44,7 +43,7 @@ NS_SWIFT_NAME(CustomProperties)
*
* @return This instance.
*/
- (instancetype)setBool:(BOOL)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
- (instancetype)setBool:(BOOL)value forKey:(NSString *)key;
/**
* Set the specified property value with the specified key.
@@ -55,7 +54,7 @@ NS_SWIFT_NAME(CustomProperties)
*
* @return This instance.
*/
- (instancetype)setDate:(NSDate *)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
- (instancetype)setDate:(NSDate *)value forKey:(NSString *)key;
/**
* Clear the property for the specified key.
@@ -64,7 +63,7 @@ NS_SWIFT_NAME(CustomProperties)
*
* @return This instance.
*/
- (instancetype)clearPropertyForKey:(NSString *)key NS_SWIFT_NAME(clearProperty(forKey:));
- (instancetype)clearPropertyForKey:(NSString *)key;
@end
@@ -1,19 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MSAC_DEVICE_H
#define MSAC_DEVICE_H
#ifndef MS_DEVICE_H
#define MS_DEVICE_H
#import <Foundation/Foundation.h>
#if __has_include(<AppCenter/MSACWrapperSdk.h>)
#import <AppCenter/MSACWrapperSdk.h>
#else
#import "MSACWrapperSdk.h"
#endif
#import "MSWrapperSdk.h"
NS_SWIFT_NAME(Device)
@interface MSACDevice : MSACWrapperSdk
@interface MSDevice : MSWrapperSdk
/*
* Name of the SDK. Consists of the name of the SDK and the platform, e.g. "appcenter.ios", "appcenter.android"
@@ -1,16 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MSAC_ENABLE_H
#define MSAC_ENABLE_H
#ifndef MS_ENABLE_H
#define MS_ENABLE_H
#import <Foundation/Foundation.h>
/**
* Protocol to define an instance that can be enabled/disabled.
*/
NS_SWIFT_NAME(Enable)
@protocol MSACEnable <NSObject>
@protocol MSEnable <NSObject>
@required
@@ -20,7 +19,7 @@ NS_SWIFT_NAME(Enable)
* @param isEnabled A boolean value set to YES to enable the instance or NO to disable it.
* @param deleteData A boolean value set to YES to delete data or NO to keep it.
*/
- (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deleteData NS_SWIFT_NAME(setEnabled(_:deleteDataOnDisabled:));
- (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deleteData;
@end
@@ -1,15 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MSAC_LOG_H
#define MSAC_LOG_H
#ifndef MS_LOG_H
#define MS_LOG_H
#import <Foundation/Foundation.h>
@class MSACDevice;
@class MSDevice;
NS_SWIFT_NAME(Log)
@protocol MSACLog <NSObject>
@protocol MSLog <NSObject>
/**
* Log type.
@@ -40,7 +39,7 @@ NS_SWIFT_NAME(Log)
/**
* Device properties associated to this log.
*/
@property(nonatomic, strong) MSACDevice *device;
@property(nonatomic, strong) MSDevice *device;
/**
* Transient object tag. For example, a log can be tagged with a transmission target. We do this currently to prevent properties being
@@ -1,19 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MSAC_LOG_WITH_PROPERTIES_H
#define MSAC_LOG_WITH_PROPERTIES_H
#ifndef MS_LOG_WITH_PROPERTIES_H
#define MS_LOG_WITH_PROPERTIES_H
#import <Foundation/Foundation.h>
#if __has_include(<AppCenter/MSACAbstractLog.h>)
#import <AppCenter/MSACAbstractLog.h>
#else
#import "MSACAbstractLog.h"
#endif
#import "MSAbstractLog.h"
NS_SWIFT_NAME(LogWithProperties)
@interface MSACLogWithProperties : MSACAbstractLog
@interface MSLogWithProperties : MSAbstractLog
/**
* Additional key/value pair parameters. [optional]
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#import <Foundation/Foundation.h>
#import "MSConstants.h"
#define MSLog(_level, _tag, _message) \
[MSLogger logMessage:_message level:_level tag:_tag file:__FILE__ function:__PRETTY_FUNCTION__ line:__LINE__]
#define MSLogAssert(tag, format, ...) \
MSLog(MSLogLevelAssert, tag, (^{ \
return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
}))
#define MSLogError(tag, format, ...) \
MSLog(MSLogLevelError, tag, (^{ \
return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
}))
#define MSLogWarning(tag, format, ...) \
MSLog(MSLogLevelWarning, tag, (^{ \
return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
}))
#define MSLogInfo(tag, format, ...) \
MSLog(MSLogLevelInfo, tag, (^{ \
return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
}))
#define MSLogDebug(tag, format, ...) \
MSLog(MSLogLevelDebug, tag, (^{ \
return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
}))
#define MSLogVerbose(tag, format, ...) \
MSLog(MSLogLevelVerbose, tag, (^{ \
return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
}))
@interface MSLogger : NSObject
+ (void)logMessage:(MSLogMessageProvider)messageProvider
level:(MSLogLevel)loglevel
tag:(NSString *)tag
file:(const char *)file
function:(const char *)function
line:(uint)line;
@end
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_SERVICE_H
#define MS_SERVICE_H
#import <Foundation/Foundation.h>
/**
* Protocol declaring service logic.
*/
@protocol MSService <NSObject>
/**
* Enable or disable this service.
* The state is persisted in the device's storage across application launches.
*
* @param isEnabled Whether this service is enabled or not.
*
* @see isEnabled
*/
+ (void)setEnabled:(BOOL)isEnabled;
/**
* Indicates whether this service is enabled.
*
* @return `YES` if this service is enabled, `NO` if it is not.
*
* @see setEnabled:
*/
+ (BOOL)isEnabled;
@end
#endif
@@ -1,25 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MSAC_SERVICE_ABSTRACT_H
#define MSAC_SERVICE_ABSTRACT_H
#ifndef MS_SERVICE_ABSTRACT_H
#define MS_SERVICE_ABSTRACT_H
#import <Foundation/Foundation.h>
#if __has_include(<AppCenter/MSACService.h>)
#import <AppCenter/MSACService.h>
#else
#import "MSACService.h"
#endif
#import "MSService.h"
@protocol MSACChannelGroupProtocol;
@protocol MSChannelGroupProtocol;
/**
* Abstraction of services common logic.
* This class is intended to be subclassed only not instantiated directly.
*/
NS_SWIFT_NAME(ServiceAbstract)
@interface MSACServiceAbstract : NSObject <MSACService>
@interface MSServiceAbstract : NSObject <MSService>
/**
* The flag indicates whether the service is started from application or not.
@@ -34,7 +29,7 @@ NS_SWIFT_NAME(ServiceAbstract)
* @param token default transmission target token for this service.
* @param fromApplication indicates whether the service started from an application or not.
*/
- (void)startWithChannelGroup:(id<MSACChannelGroupProtocol>)channelGroup
- (void)startWithChannelGroup:(id<MSChannelGroupProtocol>)channelGroup
appSecret:(NSString *)appSecret
transmissionTargetToken:(NSString *)token
fromApplication:(BOOL)fromApplication;
@@ -49,9 +44,11 @@ NS_SWIFT_NAME(ServiceAbstract)
- (void)updateConfigurationWithAppSecret:(NSString *)appSecret transmissionTargetToken:(NSString *)token;
/**
* The flag indicate whether the service needs the application secret or not.
* Checks if the service needs the application secret.
*
* @return `YES` if the application secret is required, `NO` otherwise.
*/
@property(atomic, readonly) BOOL isAppSecretRequired;
- (BOOL)isAppSecretRequired;
@end
@@ -3,19 +3,14 @@
#import <Foundation/Foundation.h>
#if __has_include(<AppCenter/MSACConstants.h>)
#import <AppCenter/MSACConstants.h>
#else
#import "MSACConstants.h"
#endif
#import "MSConstants.h"
/**
* This is a utility for producing App Center style log messages. It is only intended for use by App Center services and wrapper SDKs of App
* Center.
*/
NS_SWIFT_NAME(WrapperLogger)
@interface MSACWrapperLogger : NSObject
@interface MSWrapperLogger : NSObject
+ (void)MSACWrapperLog:(MSACLogMessageProvider)message tag:(NSString *)tag level:(MSACLogLevel)level;
+ (void)MSWrapperLog:(MSLogMessageProvider)message tag:(NSString *)tag level:(MSLogLevel)level;
@end
@@ -1,13 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MSAC_WRAPPER_SDK_H
#define MSAC_WRAPPER_SDK_H
#ifndef MS_WRAPPER_SDK_H
#define MS_WRAPPER_SDK_H
#import <Foundation/Foundation.h>
NS_SWIFT_NAME(WrapperSdk)
@interface MSACWrapperSdk : NSObject
@interface MSWrapperSdk : NSObject
/*
* Version of the wrapper SDK. When the SDK is embedding another base SDK (for example Xamarin.Android wraps Android), the Xamarin specific
@@ -0,0 +1,12 @@
framework module AppCenter {
umbrella header "AppCenter.h"
export *
module * { export * }
link framework "Foundation"
link framework "SystemConfiguration"
link framework "AppKit"
link "sqlite3"
link "z"
}
@@ -3,16 +3,14 @@
#import <Foundation/Foundation.h>
#import "MSACConstants+Flags.h"
#import "MSConstants+Flags.h"
@protocol MSACChannelUnitProtocol;
@protocol MSACChannelGroupProtocol;
@protocol MSACChannelProtocol;
@protocol MSACLog;
@protocol MSChannelUnitProtocol;
@protocol MSChannelGroupProtocol;
@protocol MSChannelProtocol;
@protocol MSLog;
NS_ASSUME_NONNULL_BEGIN
@protocol MSACChannelDelegate <NSObject>
@protocol MSChannelDelegate <NSObject>
@optional
@@ -22,14 +20,14 @@ NS_ASSUME_NONNULL_BEGIN
* @param channelGroup The channel group.
* @param channel The newly added channel.
*/
- (void)channelGroup:(id<MSACChannelGroupProtocol>)channelGroup didAddChannelUnit:(id<MSACChannelUnitProtocol>)channel;
- (void)channelGroup:(id<MSChannelGroupProtocol>)channelGroup didAddChannelUnit:(id<MSChannelUnitProtocol>)channel;
/**
* A callback that is called when a log is just enqueued. Delegates may want to prepare the log a little more before further processing.
*
* @param log The log to prepare.
*/
- (void)channel:(id<MSACChannelProtocol>)channel prepareLog:(id<MSACLog>)log;
- (void)channel:(id<MSChannelProtocol>)channel prepareLog:(id<MSLog>)log;
/**
* A callback that is called after a log is definitely prepared.
@@ -38,7 +36,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param internalId An internal Id to keep track of logs.
* @param flags Options for the log.
*/
- (void)channel:(id<MSACChannelProtocol>)channel didPrepareLog:(id<MSACLog>)log internalId:(NSString *)internalId flags:(MSACFlags)flags;
- (void)channel:(id<MSChannelProtocol>)channel didPrepareLog:(id<MSLog>)log internalId:(NSString *)internalId flags:(MSFlags)flags;
/**
* A callback that is called after a log completed the enqueueing process whether it was successful or not.
@@ -46,7 +44,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param log The log.
* @param internalId An internal Id to keep track of logs.
*/
- (void)channel:(id<MSACChannelProtocol>)channel didCompleteEnqueueingLog:(id<MSACLog>)log internalId:(NSString *)internalId;
- (void)channel:(id<MSChannelProtocol>)channel didCompleteEnqueueingLog:(id<MSLog>)log internalId:(NSString *)internalId;
/**
* Callback method that will be called before each log will be send to the server.
@@ -54,7 +52,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param channel The channel object.
* @param log The log to be sent.
*/
- (void)channel:(id<MSACChannelProtocol>)channel willSendLog:(id<MSACLog>)log;
- (void)channel:(id<MSChannelProtocol>)channel willSendLog:(id<MSLog>)log;
/**
* Callback method that will be called in case the SDK was able to send a log.
@@ -62,7 +60,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param channel The channel object.
* @param log The log to be sent.
*/
- (void)channel:(id<MSACChannelProtocol>)channel didSucceedSendingLog:(id<MSACLog>)log;
- (void)channel:(id<MSChannelProtocol>)channel didSucceedSendingLog:(id<MSLog>)log;
/**
* Callback method that will be called in case the SDK was unable to send a log.
@@ -71,7 +69,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param log The log to be sent.
* @param error The error that occured.
*/
- (void)channel:(id<MSACChannelProtocol>)channel didFailSendingLog:(id<MSACLog>)log withError:(nullable NSError *)error;
- (void)channel:(id<MSChannelProtocol>)channel didFailSendingLog:(id<MSLog>)log withError:(NSError *)error;
/**
* A callback that is called when setEnabled has been invoked.
@@ -80,7 +78,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param isEnabled The boolean that indicates enabled.
* @param deletedData The boolean that indicates deleting data on disabled.
*/
- (void)channel:(id<MSACChannelProtocol>)channel didSetEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deletedData;
- (void)channel:(id<MSChannelProtocol>)channel didSetEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deletedData;
/**
* A callback that is called when pause has been invoked.
@@ -88,7 +86,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param channel The channel.
* @param identifyingObject The identifying object used to pause the channel.
*/
- (void)channel:(id<MSACChannelProtocol>)channel didPauseWithIdentifyingObject:(id<NSObject>)identifyingObject;
- (void)channel:(id<MSChannelProtocol>)channel didPauseWithIdentifyingObject:(id<NSObject>)identifyingObject;
/**
* A callback that is called when resume has been invoked.
@@ -96,7 +94,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param channel The channel.
* @param identifyingObject The identifying object used to resume the channel.
*/
- (void)channel:(id<MSACChannelProtocol>)channel didResumeWithIdentifyingObject:(id<NSObject>)identifyingObject;
- (void)channel:(id<MSChannelProtocol>)channel didResumeWithIdentifyingObject:(id<NSObject>)identifyingObject;
/**
* Callback method that will determine if a log should be filtered out from the usual processing pipeline. If any delegate returns true, the
@@ -107,8 +105,6 @@ NS_ASSUME_NONNULL_BEGIN
*
* @return `true` if the log should be filtered out.
*/
- (BOOL)channelUnit:(id<MSACChannelUnitProtocol>)channelUnit shouldFilterLog:(id<MSACLog>)log;
- (BOOL)channelUnit:(id<MSChannelUnitProtocol>)channelUnit shouldFilterLog:(id<MSLog>)log;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_CONSTANTS_FLAGS_H
#define MS_CONSTANTS_FLAGS_H
#import <Foundation/Foundation.h>
typedef NS_OPTIONS(NSUInteger, MSFlags) {
MSFlagsNone = (0 << 0), // => 00000000
MSFlagsNormal = (1 << 0), // => 00000001
MSFlagsCritical = (1 << 1), // => 00000010
MSFlagsPersistenceNormal DEPRECATED_MSG_ATTRIBUTE("please use MSFlagsNormal") = MSFlagsNormal,
MSFlagsPersistenceCritical DEPRECATED_MSG_ATTRIBUTE("please use MSFlagsCritical") = MSFlagsCritical,
MSFlagsDefault = MSFlagsNormal
};
#endif
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>19F101</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>AppCenter</string>
<key>CFBundleIdentifier</key>
<string>com.microsoft.appcenter</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>AppCenter</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>11C505</string>
<key>DTPlatformVersion</key>
<string>GM</string>
<key>DTSDKBuild</key>
<string>19B90</string>
<key>DTSDKName</key>
<string>macosx10.15</string>
<key>DTXcode</key>
<string>1130</string>
<key>DTXcodeBuild</key>
<string>11C505</string>
<key>LSMinimumSystemVersion</key>
<string>10.9</string>
</dict>
</plist>
@@ -0,0 +1 @@
Versions/Current/AppCenterCrashes
@@ -0,0 +1 @@
Versions/Current/Headers
@@ -0,0 +1 @@
Versions/Current/Modules
@@ -0,0 +1 @@
Versions/Current/Resources
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#import <Foundation/Foundation.h>
#import "MSCrashHandlerSetupDelegate.h"
#import "MSCrashes.h"
#import "MSCrashesDelegate.h"
#import "MSErrorAttachmentLog+Utility.h"
#import "MSErrorAttachmentLog.h"
#import "MSWrapperCrashesHelper.h"
@@ -1,13 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MSAC_ABSTRACT_LOG_H
#define MSAC_ABSTRACT_LOG_H
#ifndef MS_ABSTRACT_LOG_H
#define MS_ABSTRACT_LOG_H
#import <Foundation/Foundation.h>
NS_SWIFT_NAME(AbstractLog)
@interface MSACAbstractLog : NSObject
@interface MSAbstractLog : NSObject
@end
@@ -6,8 +6,7 @@
/**
* This is required for Wrapper SDKs that need to provide custom behavior surrounding the setup of crash handlers.
*/
NS_SWIFT_NAME(CrashHandlerSetupDelegate)
@protocol MSACCrashHandlerSetupDelegate <NSObject>
@protocol MSCrashHandlerSetupDelegate <NSObject>
@optional
@@ -1,101 +1,63 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#if __has_include(<AppCenter/MSACServiceAbstract.h>)
#import <AppCenter/MSACServiceAbstract.h>
#else
#import "MSACServiceAbstract.h"
#endif
#import "MSErrorReport.h"
#import "MSServiceAbstract.h"
#if __has_include(<AppCenterCrashes/MSACErrorReport.h>)
#import <AppCenterCrashes/MSACErrorReport.h>
#else
#import "MSACErrorReport.h"
#endif
@class MSACCrashesDelegate;
@class MSACExceptionModel;
@class MSACErrorAttachmentLog;
@class MSCrashesDelegate;
/**
* Custom block that handles the alert that prompts the user whether crash reports need to be processed or not.
*
* @return Returns YES to discard crash reports, otherwise NO.
*/
typedef BOOL (^MSACUserConfirmationHandler)(NSArray<MSACErrorReport *> *_Nonnull errorReports) NS_SWIFT_NAME(UserConfirmationHandler);
typedef BOOL (^MSUserConfirmationHandler)(NSArray<MSErrorReport *> *_Nonnull errorReports);
/**
* Error Logging status.
*/
typedef NS_ENUM(NSUInteger, MSACErrorLogSetting) {
typedef NS_ENUM(NSUInteger, MSErrorLogSetting) {
/**
* Crash reporting is disabled.
*/
MSACErrorLogSettingDisabled = 0,
MSErrorLogSettingDisabled = 0,
/**
* User is asked each time before sending error logs.
*/
MSACErrorLogSettingAlwaysAsk = 1,
MSErrorLogSettingAlwaysAsk = 1,
/**
* Each error log is send automatically.
*/
MSACErrorLogSettingAutoSend = 2
} NS_SWIFT_NAME(ErrorLogSetting);
MSErrorLogSettingAutoSend = 2
};
/**
* Crash Manager alert user input.
*/
typedef NS_ENUM(NSUInteger, MSACUserConfirmation) {
typedef NS_ENUM(NSUInteger, MSUserConfirmation) {
/**
* User chose not to send the crash report.
*/
MSACUserConfirmationDontSend = 0,
MSUserConfirmationDontSend = 0,
/**
* User wants the crash report to be sent.
*/
MSACUserConfirmationSend = 1,
MSUserConfirmationSend = 1,
/**
* User wants to send all error logs.
*/
MSACUserConfirmationAlways = 2
} NS_SWIFT_NAME(UserConfirmation);
MSUserConfirmationAlways = 2
};
@protocol MSACCrashesDelegate;
@protocol MSCrashesDelegate;
NS_SWIFT_NAME(Crashes)
@interface MSACCrashes : MSACServiceAbstract
/**
* Track handled error.
*
* @param error error.
* @param properties dictionary of properties.
* @param attachments a list of attachments.
*
* @return handled error ID.
*/
+ (NSString *_Nonnull)trackError:(NSError *_Nonnull)error
withProperties:(nullable NSDictionary<NSString *, NSString *> *)properties
attachments:(nullable NSArray<MSACErrorAttachmentLog *> *)attachments NS_SWIFT_NAME(trackError(_:properties:attachments:));
/**
* Track handled exception from custom exception model.
*
* @param exception custom model exception.
* @param properties dictionary of properties.
* @param attachments a list of attachments.
*
* @return handled error ID.
*/
+ (NSString *_Nonnull)trackException:(MSACExceptionModel *_Nonnull)exception
withProperties:(nullable NSDictionary<NSString *, NSString *> *)properties
attachments:(nullable NSArray<MSACErrorAttachmentLog *> *)attachments NS_SWIFT_NAME(trackException(_:properties:attachments:));
@interface MSCrashes : MSServiceAbstract
///-----------------------------------------------------------------------------
/// @name Testing Crashes Feature
@@ -125,19 +87,19 @@ NS_SWIFT_NAME(Crashes)
*
* @return Returns YES is the app has crashed in the last session.
*/
@property(class, readonly, nonatomic) BOOL hasCrashedInLastSession;
+ (BOOL)hasCrashedInLastSession;
/**
* Check if the app received memory warning in the last session.
*
* @return Returns YES is the app received memory warning in the last session.
*/
@property(class, readonly, nonatomic) BOOL hasReceivedMemoryWarningInLastSession;
+ (BOOL)hasReceivedMemoryWarningInLastSession;
/**
* Provides details about the crash that occurred in the last app session
*/
@property(class, nullable, readonly, nonatomic) MSACErrorReport *lastSessionCrashReport;
+ (nullable MSErrorReport *)lastSessionCrashReport;
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
/**
@@ -163,13 +125,13 @@ NS_SWIFT_NAME(Crashes)
* want to disable the Mach exception handler, you should call this method _BEFORE_ starting the SDK. Your typical setup code would look
* like this:
*
* `[MSACCrashes disableMachExceptionHandler]`;
* `[MSACAppCenter start:@"YOUR_APP_ID" withServices:@[[MSACCrashes class]]];`
* `[MSCrashes disableMachExceptionHandler]`;
* `[MSAppCenter start:@"YOUR_APP_ID" withServices:@[[MSCrashes class]]];`
*
* or if you are using Swift:
*
* `MSACCrashes.disableMachExceptionHandler()`
* `MSACAppCenter.start("YOUR_APP_ID", withServices: [MSACAnalytics.self, MSACCrashes.self])`
* `MSCrashes.disableMachExceptionHandler()`
* `MSAppCenter.start("YOUR_APP_ID", withServices: [MSAnalytics.self, MSCrashes.self])`
*
* tvOS does not support the Mach exception handler, thus crashes that are caused by stack overflows cannot be detected. As a result,
* disabling the Mach exception server is not available in the tvOS SDK.
@@ -182,26 +144,28 @@ NS_SWIFT_NAME(Crashes)
/**
* Set the delegate
* Defines the class that implements the optional protocol `MSACCrashesDelegate`.
* Defines the class that implements the optional protocol `MSCrashesDelegate`.
*
* @see MSACCrashesDelegate
* @see MSCrashesDelegate
*/
@property(class, nonatomic, weak) id<MSACCrashesDelegate> _Nullable delegate;
+ (void)setDelegate:(_Nullable id<MSCrashesDelegate>)delegate;
/**
* Set a user confirmation handler that is invoked right before processing crash reports to determine whether sending crash reports or not.
*
* @see MSACUserConfirmationHandler
* @param userConfirmationHandler A handler for user confirmation.
*
* @see MSUserConfirmationHandler
*/
@property(class, nonatomic) MSACUserConfirmationHandler _Nullable userConfirmationHandler;
+ (void)setUserConfirmationHandler:(_Nullable MSUserConfirmationHandler)userConfirmationHandler;
/**
* Notify SDK with a confirmation to handle the crash report.
*
* @param userConfirmation A user confirmation.
*
* @see MSACUserConfirmation.
* @see MSUserConfirmation.
*/
+ (void)notifyWithUserConfirmation:(MSACUserConfirmation)userConfirmation;
+ (void)notifyWithUserConfirmation:(MSUserConfirmation)userConfirmation;
@end
@@ -3,68 +3,63 @@
#import <Foundation/Foundation.h>
@class MSACCrashes;
@class MSACErrorReport;
@class MSACErrorAttachmentLog;
@class MSCrashes;
@class MSErrorReport;
@class MSErrorAttachmentLog;
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(CrashesDelegate)
@protocol MSACCrashesDelegate <NSObject>
@protocol MSCrashesDelegate <NSObject>
@optional
/**
* Callback method that will be called before processing errors.
*
* @param crashes The instance of MSACCrashes.
* @param crashes The instance of MSCrashes.
* @param errorReport The errorReport that will be sent.
*
* @discussion Crashes will send logs to the server or discard/delete logs based on this method's return value.
*/
- (BOOL)crashes:(MSACCrashes *)crashes shouldProcessErrorReport:(MSACErrorReport *)errorReport NS_SWIFT_NAME(crashes(_:shouldProcess:));
- (BOOL)crashes:(MSCrashes *)crashes shouldProcessErrorReport:(MSErrorReport *)errorReport;
/**
* Callback method that will be called before each error will be send to the server.
*
* @param crashes The instance of MSACCrashes.
* @param crashes The instance of MSCrashes.
* @param errorReport The errorReport that will be sent.
*
* @discussion Use this callback to display custom UI while crashes are sent to the server.
*/
- (void)crashes:(MSACCrashes *)crashes willSendErrorReport:(MSACErrorReport *)errorReport;
/**
* Callback method that will be called after the SDK successfully sent an error report to the server.
*
* @param crashes The instance of MSACCrashes.
* @param errorReport The errorReport that App Center sent.
*
* @discussion Use this method to hide your custom UI.
*/
- (void)crashes:(MSACCrashes *)crashes didSucceedSendingErrorReport:(MSACErrorReport *)errorReport;
- (void)crashes:(MSCrashes *)crashes willSendErrorReport:(MSErrorReport *)errorReport;
/**
* Callback method that will be called in case the SDK was unable to send an error report to the server.
*
* @param crashes The instance of MSACCrashes.
* @param crashes The instance of MSCrashes.
* @param errorReport The errorReport that App Center sent.
*
* @discussion Use this method to hide your custom UI.
*/
- (void)crashes:(MSCrashes *)crashes didSucceedSendingErrorReport:(MSErrorReport *)errorReport;
/**
* Callback method that will be called in case the SDK was unable to send an error report to the server.
*
* @param crashes The instance of MSCrashes.
* @param errorReport The errorReport that App Center tried to send.
* @param error The error that occurred.
*/
- (void)crashes:(MSACCrashes *)crashes didFailSendingErrorReport:(MSACErrorReport *)errorReport withError:(nullable NSError *)error;
- (void)crashes:(MSCrashes *)crashes didFailSendingErrorReport:(MSErrorReport *)errorReport withError:(NSError *)error;
/**
* Method to get the attachments associated to an error report.
*
* @param crashes The instance of MSACCrashes.
* @param crashes The instance of MSCrashes.
* @param errorReport The errorReport associated with the returned attachments.
*
* @return The attachments associated with the given error report or nil if the error report doesn't have any attachments.
*
* @discussion Implement this method if you want attachments to the given error report.
*/
- (nullable NSArray<MSACErrorAttachmentLog *> *)attachmentsWithCrashes:(MSACCrashes *)crashes forErrorReport:(MSACErrorReport *)errorReport;
- (NSArray<MSErrorAttachmentLog *> *)attachmentsWithCrashes:(MSCrashes *)crashes forErrorReport:(MSErrorReport *)errorReport;
@end
NS_ASSUME_NONNULL_END
@@ -1,16 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#if __has_include(<AppCenterCrashes/MSACErrorAttachmentLog.h>)
#import <AppCenterCrashes/MSACErrorAttachmentLog.h>
#else
#import "MSACErrorAttachmentLog.h"
#endif
#import "MSErrorAttachmentLog.h"
// Exporting symbols for category.
extern NSString *MSACMSACErrorLogAttachmentLogUtilityCategory;
extern NSString *MSMSErrorLogAttachmentLogUtilityCategory;
@interface MSACErrorAttachmentLog (Utility)
@interface MSErrorAttachmentLog (Utility)
/**
* Create an attachment with a given filename and text.
@@ -18,9 +14,9 @@ extern NSString *MSACMSACErrorLogAttachmentLogUtilityCategory;
* @param filename The filename the attachment should get. If nil will get an automatically generated filename.
* @param text The attachment text.
*
* @return An instance of `MSACErrorAttachmentLog`.
* @return An instance of `MSErrorAttachmentLog`.
*/
+ (MSACErrorAttachmentLog *)attachmentWithText:(NSString *)text filename:(NSString *)filename;
+ (MSErrorAttachmentLog *)attachmentWithText:(NSString *)text filename:(NSString *)filename;
/**
* Create an attachment with a given filename and `NSData` object.
@@ -29,8 +25,8 @@ extern NSString *MSACMSACErrorLogAttachmentLogUtilityCategory;
* @param data The attachment data as NSData.
* @param contentType The content type of your data as MIME type.
*
* @return An instance of `MSACErrorAttachmentLog`.
* @return An instance of `MSErrorAttachmentLog`.
*/
+ (MSACErrorAttachmentLog *)attachmentWithBinary:(NSData *)data filename:(NSString *)filename contentType:(NSString *)contentType;
+ (MSErrorAttachmentLog *)attachmentWithBinary:(NSData *)data filename:(NSString *)filename contentType:(NSString *)contentType;
@end
@@ -3,17 +3,12 @@
#import <Foundation/Foundation.h>
#if __has_include(<AppCenter/MSACAbstractLog.h>)
#import <AppCenter/MSACAbstractLog.h>
#else
#import "MSACAbstractLog.h"
#endif
#import "MSAbstractLog.h"
/**
* Error attachment log.
*/
NS_SWIFT_NAME(ErrorAttachmentLog)
@interface MSACErrorAttachmentLog : MSACAbstractLog
@interface MSErrorAttachmentLog : MSAbstractLog
/**
* Content type (text/plain for text).
@@ -37,7 +32,7 @@ NS_SWIFT_NAME(ErrorAttachmentLog)
* @param data The attachment data as `NSData`.
* @param contentType The content type of your data as MIME type.
*
* @return An instance of `MSACErrorAttachmentLog`.
* @return An instance of `MSErrorAttachmentLog`.
*/
- (instancetype)initWithFilename:(NSString *)filename attachmentBinary:(NSData *)data contentType:(NSString *)contentType;
@@ -47,7 +42,7 @@ NS_SWIFT_NAME(ErrorAttachmentLog)
* @param filename The filename the attachment should get. If nil will get an automatically generated filename.
* @param text The attachment text.
*
* @return An instance of `MSACErrorAttachmentLog`.
* @return An instance of `MSErrorAttachmentLog`.
*/
- (instancetype)initWithFilename:(NSString *)filename attachmentText:(NSString *)text;
@@ -3,10 +3,9 @@
#import <Foundation/Foundation.h>
@class MSACDevice;
@class MSDevice;
NS_SWIFT_NAME(ErrorReport)
@interface MSACErrorReport : NSObject
@interface MSErrorReport : NSObject
/**
* UUID for the crash report.
@@ -46,7 +45,7 @@ NS_SWIFT_NAME(ErrorReport)
/**
* Device information of the app when it crashed.
*/
@property(nonatomic, readonly, strong) MSACDevice *device;
@property(nonatomic, readonly, strong) MSDevice *device;
/**
* Identifier of the app process that crashed.
@@ -60,8 +59,10 @@ NS_SWIFT_NAME(ErrorReport)
* thread for too long, or other reasons. See Apple documentation:
* https://developer.apple.com/library/ios/qa/qa1693/_index.html.
*
* @see `[MSACCrashes didReceiveMemoryWarningInLastSession]`
* @return YES if the details represent an app kill instead of a crash.
*
* @see `[MSCrashes didReceiveMemoryWarningInLastSession]`
*/
@property(nonatomic, readonly) BOOL isAppKill;
- (BOOL)isAppKill;
@end
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_SERVICE_H
#define MS_SERVICE_H
#import <Foundation/Foundation.h>
/**
* Protocol declaring service logic.
*/
@protocol MSService <NSObject>
/**
* Enable or disable this service.
* The state is persisted in the device's storage across application launches.
*
* @param isEnabled Whether this service is enabled or not.
*
* @see isEnabled
*/
+ (void)setEnabled:(BOOL)isEnabled;
/**
* Indicates whether this service is enabled.
*
* @return `YES` if this service is enabled, `NO` if it is not.
*
* @see setEnabled:
*/
+ (BOOL)isEnabled;
@end
#endif
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_SERVICE_ABSTRACT_H
#define MS_SERVICE_ABSTRACT_H
#import <Foundation/Foundation.h>
#import "MSService.h"
@protocol MSChannelGroupProtocol;
/**
* Abstraction of services common logic.
* This class is intended to be subclassed only not instantiated directly.
*/
@interface MSServiceAbstract : NSObject <MSService>
/**
* The flag indicates whether the service is started from application or not.
*/
@property(nonatomic, assign) BOOL startedFromApplication;
/**
* Start this service with a channel group. Also sets the flag that indicates that a service has been started.
*
* @param channelGroup channel group used to persist and send logs.
* @param appSecret app secret for the SDK.
* @param token default transmission target token for this service.
* @param fromApplication indicates whether the service started from an application or not.
*/
- (void)startWithChannelGroup:(id<MSChannelGroupProtocol>)channelGroup
appSecret:(NSString *)appSecret
transmissionTargetToken:(NSString *)token
fromApplication:(BOOL)fromApplication;
/**
* Update configuration when the service requires to start again. This method should only be called if the service is started from libraries
* and then is being started from an application.
*
* @param appSecret app secret for the SDK.
* @param token default transmission target token for this service.
*/
- (void)updateConfigurationWithAppSecret:(NSString *)appSecret transmissionTargetToken:(NSString *)token;
/**
* Checks if the service needs the application secret.
*
* @return `YES` if the application secret is required, `NO` otherwise.
*/
- (BOOL)isAppSecretRequired;
@end
#endif
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#import <Foundation/Foundation.h>
#import "MSCrashHandlerSetupDelegate.h"
NS_ASSUME_NONNULL_BEGIN
@class MSErrorReport;
@class MSErrorAttachmentLog;
@class MSException;
/**
* This general class allows wrappers to supplement the Crashes SDK with their own behavior.
*/
@interface MSWrapperCrashesHelper : NSObject
/**
* Sets the crash handler setup delegate.
*
* @param delegate The delegate to set.
*/
+ (void)setCrashHandlerSetupDelegate:(id<MSCrashHandlerSetupDelegate>)delegate;
/**
* Gets the crash handler setup delegate.
*
* @return The delegate being used by Crashes.
*/
+ (id<MSCrashHandlerSetupDelegate>)getCrashHandlerSetupDelegate;
/**
* Enables or disables automatic crash processing.
*
* @param automaticProcessing Passing NO causes SDK not to send reports immediately, even if "Always Send" is true.
*/
+ (void)setAutomaticProcessing:(BOOL)automaticProcessing;
/**
* Gets a list of unprocessed crash reports. Will block until the service starts.
*
* @return An array of unprocessed error reports.
*/
+ (NSArray<MSErrorReport *> *)unprocessedCrashReports;
/**
* Resumes processing for a given subset of the unprocessed reports.
*
* @param filteredIds An array containing the errorId/incidentIdentifier of each report that should be sent.
*
* @return YES if should "Always Send" is true.
*/
+ (BOOL)sendCrashReportsOrAwaitUserConfirmationForFilteredIds:(NSArray<NSString *> *)filteredIds;
/**
* Sends error attachments for a particular error report.
*
* @param errorAttachments An array of error attachments that should be sent.
* @param incidentIdentifier The identifier of the error report that the attachments will be associated with.
*/
+ (void)sendErrorAttachments:(NSArray<MSErrorAttachmentLog *> *)errorAttachments withIncidentIdentifier:(NSString *)incidentIdentifier;
/**
* Track handled exception directly as model form.
* This API is used by wrapper SDKs.
*
* @param exception model form exception.
* @param properties dictionary of properties.
* @param attachments a list of attachments.
*
* @return handled error ID.
*/
+ (nullable NSString *)trackModelException:(MSException *)exception
withProperties:(nullable NSDictionary<NSString *, NSString *> *)properties
withAttachments:(nullable NSArray<MSErrorAttachmentLog *> *)attachments;
/**
* Get a generic error report representation for an handled exception.
* This API is used by wrapper SDKs.
*
* @param errorID handled error ID.
*
* @return an error report.
*/
+ (MSErrorReport *)buildHandledErrorReportWithErrorID:(NSString *)errorID;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,10 @@
framework module AppCenterCrashes {
umbrella header "AppCenterCrashes.h"
export *
module * { export * }
link framework "Foundation"
link "c++"
link "z"
}
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>19F101</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>AppCenterCrashes</string>
<key>CFBundleIdentifier</key>
<string>com.microsoft.appcenter.crashes</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>AppCenterCrashes</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>11C505</string>
<key>DTPlatformVersion</key>
<string>GM</string>
<key>DTSDKBuild</key>
<string>19B90</string>
<key>DTSDKName</key>
<string>macosx10.15</string>
<key>DTXcode</key>
<string>1130</string>
<key>DTXcodeBuild</key>
<string>11C505</string>
<key>LSMinimumSystemVersion</key>
<string>10.9</string>
</dict>
</plist>
@@ -0,0 +1 @@
A
+10
View File
@@ -0,0 +1,10 @@
//
// LetsMove.h
// LetsMove
//
// Created by Matt Prowse on 14/05/2016.
//
// The contents of this file are dedicated to the public domain.
#import <Cocoa/Cocoa.h>
#import <LetsMove/PFMoveApplication.h>
+32
View File
@@ -0,0 +1,32 @@
//
// PFMoveApplication.h, version 1.24
// LetsMove
//
// Created by Andy Kim at Potion Factory LLC on 9/17/09
//
// The contents of this file are dedicated to the public domain.
#ifdef __cplusplus
extern "C" {
#endif
#import <Foundation/Foundation.h>
/**
Moves the running application to ~/Applications or /Applications if the former does not exist.
After the move, it relaunches app from the new location.
DOES NOT work for sandboxed applications.
Call from \c NSApplication's delegate method \c -applicationWillFinishLaunching: method. */
void PFMoveToApplicationsFolderIfNecessary(void);
/**
Check whether an app move is currently in progress.
Returns YES if LetsMove is currently in-progress trying to move the app to the Applications folder, or NO otherwise.
This can be used to work around a crash with apps that terminate after last window is closed.
See https://github.com/potionfactory/LetsMove/issues/64 for details. */
BOOL PFMoveIsInProgress(void);
#ifdef __cplusplus
}
#endif
+563
View File
@@ -0,0 +1,563 @@
//
// PFMoveApplication.m, version 1.24
// LetsMove
//
// Created by Andy Kim at Potion Factory LLC on 9/17/09
//
// The contents of this file are dedicated to the public domain.
#import "PFMoveApplication.h"
#import <AppKit/AppKit.h>
#import <Security/Security.h>
#import <dlfcn.h>
#import <sys/mount.h>
@interface LetsMove : NSObject
@end
@implementation LetsMove
+ (NSBundle *)bundle {
return [NSBundle bundleForClass:self];
}
@end
// Strings
// These are macros to be able to use custom i18n tools
#define _I10NS(nsstr) NSLocalizedStringFromTableInBundle(nsstr, @"MoveApplication", [LetsMove bundle], nil)
#define kStrMoveApplicationCouldNotMove _I10NS(@"Could not move to Applications folder")
#define kStrMoveApplicationQuestionTitle _I10NS(@"Move to Applications folder?")
#define kStrMoveApplicationQuestionTitleHome _I10NS(@"Move to Applications folder in your Home folder?")
#define kStrMoveApplicationQuestionMessage _I10NS(@"I can move myself to the Applications folder if you'd like.")
#define kStrMoveApplicationButtonMove _I10NS(@"Move to Applications Folder")
#define kStrMoveApplicationButtonDoNotMove _I10NS(@"Do Not Move")
#define kStrMoveApplicationQuestionInfoWillRequirePasswd _I10NS(@"Note that this will require an administrator password.")
#define kStrMoveApplicationQuestionInfoInDownloadsFolder _I10NS(@"This will keep your Downloads folder uncluttered.")
// Needs to be defined for compiling under 10.5 SDK
#ifndef NSAppKitVersionNumber10_5
#define NSAppKitVersionNumber10_5 949
#endif
// By default, we use a small control/font for the suppression button.
// If you prefer to use the system default (to match your other alerts),
// set this to 0.
#define PFUseSmallAlertSuppressCheckbox 1
static NSString *AlertSuppressKey = @"moveToApplicationsFolderAlertSuppress";
static BOOL MoveInProgress = NO;
// Helper functions
static NSString *PreferredInstallLocation(BOOL *isUserDirectory);
static BOOL IsInApplicationsFolder(NSString *path);
static BOOL IsInDownloadsFolder(NSString *path);
static BOOL IsApplicationAtPathRunning(NSString *path);
static BOOL IsApplicationAtPathNested(NSString *path);
static NSString *ContainingDiskImageDevice(NSString *path);
static BOOL Trash(NSString *path);
static BOOL DeleteOrTrash(NSString *path);
static BOOL AuthorizedInstall(NSString *srcPath, NSString *dstPath, BOOL *canceled);
static BOOL CopyBundle(NSString *srcPath, NSString *dstPath);
static NSString *ShellQuotedString(NSString *string);
static void Relaunch(NSString *destinationPath);
// Main worker function
void PFMoveToApplicationsFolderIfNecessary(void) {
// Make sure to do our work on the main thread.
// Apparently Electron apps need this for things to work properly.
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
PFMoveToApplicationsFolderIfNecessary();
});
return;
}
// Skip if user suppressed the alert before
if ([[NSUserDefaults standardUserDefaults] boolForKey:AlertSuppressKey]) return;
// Path of the bundle
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
// Check if the bundle is embedded in another application
BOOL isNestedApplication = IsApplicationAtPathNested(bundlePath);
// Skip if the application is already in some Applications folder,
// unless it's inside another app's bundle.
if (IsInApplicationsFolder(bundlePath) && !isNestedApplication) return;
// OK, looks like we'll need to do a move - set the status variable appropriately
MoveInProgress = YES;
// File Manager
NSFileManager *fm = [NSFileManager defaultManager];
// Are we on a disk image?
NSString *diskImageDevice = ContainingDiskImageDevice(bundlePath);
// Since we are good to go, get the preferred installation directory.
BOOL installToUserApplications = NO;
NSString *applicationsDirectory = PreferredInstallLocation(&installToUserApplications);
NSString *bundleName = [bundlePath lastPathComponent];
NSString *destinationPath = [applicationsDirectory stringByAppendingPathComponent:bundleName];
// Check if we need admin password to write to the Applications directory
BOOL needAuthorization = ([fm isWritableFileAtPath:applicationsDirectory] == NO);
// Check if the destination bundle is already there but not writable
needAuthorization |= ([fm fileExistsAtPath:destinationPath] && ![fm isWritableFileAtPath:destinationPath]);
// Setup the alert
NSAlert *alert = [[[NSAlert alloc] init] autorelease];
{
NSString *informativeText = nil;
[alert setMessageText:(installToUserApplications ? kStrMoveApplicationQuestionTitleHome : kStrMoveApplicationQuestionTitle)];
informativeText = kStrMoveApplicationQuestionMessage;
if (needAuthorization) {
informativeText = [informativeText stringByAppendingString:@" "];
informativeText = [informativeText stringByAppendingString:kStrMoveApplicationQuestionInfoWillRequirePasswd];
}
else if (IsInDownloadsFolder(bundlePath)) {
// Don't mention this stuff if we need authentication. The informative text is long enough as it is in that case.
informativeText = [informativeText stringByAppendingString:@" "];
informativeText = [informativeText stringByAppendingString:kStrMoveApplicationQuestionInfoInDownloadsFolder];
}
[alert setInformativeText:informativeText];
// Add accept button
[alert addButtonWithTitle:kStrMoveApplicationButtonMove];
// Add deny button
NSButton *cancelButton = [alert addButtonWithTitle:kStrMoveApplicationButtonDoNotMove];
[cancelButton setKeyEquivalent:[NSString stringWithFormat:@"%C", 0x1b]]; // Escape key
// Setup suppression button
[alert setShowsSuppressionButton:YES];
if (PFUseSmallAlertSuppressCheckbox) {
NSCell *cell = [[alert suppressionButton] cell];
[cell setControlSize:NSSmallControlSize];
[cell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
}
}
// Activate app -- work-around for focus issues related to "scary file from internet" OS dialog.
if (![NSApp isActive]) {
[NSApp activateIgnoringOtherApps:YES];
}
if ([alert runModal] == NSAlertFirstButtonReturn) {
NSLog(@"INFO -- Moving myself to the Applications folder");
// Move
if (needAuthorization) {
BOOL authorizationCanceled;
if (!AuthorizedInstall(bundlePath, destinationPath, &authorizationCanceled)) {
if (authorizationCanceled) {
NSLog(@"INFO -- Not moving because user canceled authorization");
MoveInProgress = NO;
return;
}
else {
NSLog(@"ERROR -- Could not copy myself to /Applications with authorization");
goto fail;
}
}
}
else {
// If a copy already exists in the Applications folder, put it in the Trash
if ([fm fileExistsAtPath:destinationPath]) {
// But first, make sure that it's not running
if (IsApplicationAtPathRunning(destinationPath)) {
// Give the running app focus and terminate myself
NSLog(@"INFO -- Switching to an already running version");
[[NSTask launchedTaskWithLaunchPath:@"/usr/bin/open" arguments:[NSArray arrayWithObject:destinationPath]] waitUntilExit];
MoveInProgress = NO;
exit(0);
}
else {
if (!Trash([applicationsDirectory stringByAppendingPathComponent:bundleName]))
goto fail;
}
}
if (!CopyBundle(bundlePath, destinationPath)) {
NSLog(@"ERROR -- Could not copy myself to %@", destinationPath);
goto fail;
}
}
// Trash the original app. It's okay if this fails.
// NOTE: This final delete does not work if the source bundle is in a network mounted volume.
// Calling rm or file manager's delete method doesn't work either. It's unlikely to happen
// but it'd be great if someone could fix this.
if (!isNestedApplication && diskImageDevice == nil && !DeleteOrTrash(bundlePath)) {
NSLog(@"WARNING -- Could not delete application after moving it to Applications folder");
}
// Relaunch.
Relaunch(destinationPath);
// Launched from within a disk image? -- unmount (if no files are open after 5 seconds,
// otherwise leave it mounted).
if (diskImageDevice && !isNestedApplication) {
NSString *script = [NSString stringWithFormat:@"(/bin/sleep 5 && /usr/bin/hdiutil detach %@) &", ShellQuotedString(diskImageDevice)];
[NSTask launchedTaskWithLaunchPath:@"/bin/sh" arguments:[NSArray arrayWithObjects:@"-c", script, nil]];
}
MoveInProgress = NO;
exit(0);
}
// Save the alert suppress preference if checked
else if ([[alert suppressionButton] state] == NSOnState) {
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:AlertSuppressKey];
}
MoveInProgress = NO;
return;
fail:
{
// Show failure message
alert = [[[NSAlert alloc] init] autorelease];
[alert setMessageText:kStrMoveApplicationCouldNotMove];
[alert runModal];
MoveInProgress = NO;
}
}
BOOL PFMoveIsInProgress() {
return MoveInProgress;
}
#pragma mark -
#pragma mark Helper Functions
static NSString *PreferredInstallLocation(BOOL *isUserDirectory) {
// Return the preferred install location.
// Assume that if the user has a ~/Applications folder, they'd prefer their
// applications to go there.
// NSFileManager *fm = [NSFileManager defaultManager];
//
// NSArray *userApplicationsDirs = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSUserDomainMask, YES);
//
// if ([userApplicationsDirs count] > 0) {
// NSString *userApplicationsDir = [userApplicationsDirs objectAtIndex:0];
// BOOL isDirectory;
//
// if ([fm fileExistsAtPath:userApplicationsDir isDirectory:&isDirectory] && isDirectory) {
// // User Applications directory exists. Get the directory contents.
// NSArray *contents = [fm contentsOfDirectoryAtPath:userApplicationsDir error:NULL];
//
// // Check if there is at least one ".app" inside the directory.
// for (NSString *contentsPath in contents) {
// if ([[contentsPath pathExtension] isEqualToString:@"app"]) {
// if (isUserDirectory) *isUserDirectory = YES;
// return [userApplicationsDir stringByResolvingSymlinksInPath];
// }
// }
// }
// }
// No user Applications directory in use. Return the machine local Applications directory
if (isUserDirectory) *isUserDirectory = NO;
return [[NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSLocalDomainMask, YES) lastObject] stringByResolvingSymlinksInPath];
}
static BOOL IsInApplicationsFolder(NSString *path) {
// Check all the normal Application directories
NSArray *applicationDirs = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSAllDomainsMask, YES);
for (NSString *appDir in applicationDirs) {
if ([path hasPrefix:appDir]) return YES;
}
// Also, handle the case that the user has some other Application directory (perhaps on a separate data partition).
if ([[path pathComponents] containsObject:@"Applications"]) return YES;
return NO;
}
static BOOL IsInDownloadsFolder(NSString *path) {
NSArray *downloadDirs = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSAllDomainsMask, YES);
for (NSString *downloadsDirPath in downloadDirs) {
if ([path hasPrefix:downloadsDirPath]) return YES;
}
return NO;
}
static BOOL IsApplicationAtPathRunning(NSString *bundlePath) {
bundlePath = [bundlePath stringByStandardizingPath];
#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
// Use the new API on 10.6 or higher to determine if the app is already running
if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5) {
for (NSRunningApplication *runningApplication in [[NSWorkspace sharedWorkspace] runningApplications]) {
NSString *runningAppBundlePath = [[[runningApplication bundleURL] path] stringByStandardizingPath];
if ([runningAppBundlePath isEqualToString:bundlePath]) {
return YES;
}
}
return NO;
}
#endif
// Use the shell to determine if the app is already running on systems 10.5 or lower
NSString *script = [NSString stringWithFormat:@"/bin/ps ax -o comm | /usr/bin/grep %@/ | /usr/bin/grep -v grep >/dev/null", ShellQuotedString(bundlePath)];
NSTask *task = [NSTask launchedTaskWithLaunchPath:@"/bin/sh" arguments:[NSArray arrayWithObjects:@"-c", script, nil]];
[task waitUntilExit];
// If the task terminated with status 0, it means that the final grep produced 1 or more lines of output.
// Which means that the app is already running
return [task terminationStatus] == 0;
}
static BOOL IsApplicationAtPathNested(NSString *path) {
NSString *containingPath = [path stringByDeletingLastPathComponent];
NSArray *components = [containingPath pathComponents];
for (NSString *component in components) {
if ([[component pathExtension] isEqualToString:@"app"]) {
return YES;
}
}
return NO;
}
static NSString *ContainingDiskImageDevice(NSString *path) {
NSString *containingPath = [path stringByDeletingLastPathComponent];
struct statfs fs;
if (statfs([containingPath fileSystemRepresentation], &fs) || (fs.f_flags & MNT_ROOTFS))
return nil;
NSString *device = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:fs.f_mntfromname length:strlen(fs.f_mntfromname)];
NSTask *hdiutil = [[[NSTask alloc] init] autorelease];
[hdiutil setLaunchPath:@"/usr/bin/hdiutil"];
[hdiutil setArguments:[NSArray arrayWithObjects:@"info", @"-plist", nil]];
[hdiutil setStandardOutput:[NSPipe pipe]];
[hdiutil launch];
[hdiutil waitUntilExit];
NSData *data = [[[hdiutil standardOutput] fileHandleForReading] readDataToEndOfFile];
NSDictionary *info = nil;
#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5) {
info = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:NULL];
}
else {
#endif
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10
info = [NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:NULL errorDescription:NULL];
#endif
#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
}
#endif
if (![info isKindOfClass:[NSDictionary class]]) return nil;
NSArray *images = (NSArray *)[info objectForKey:@"images"];
if (![images isKindOfClass:[NSArray class]]) return nil;
for (NSDictionary *image in images) {
if (![image isKindOfClass:[NSDictionary class]]) return nil;
id systemEntities = [image objectForKey:@"system-entities"];
if (![systemEntities isKindOfClass:[NSArray class]]) return nil;
for (NSDictionary *systemEntity in systemEntities) {
if (![systemEntity isKindOfClass:[NSDictionary class]]) return nil;
NSString *devEntry = [systemEntity objectForKey:@"dev-entry"];
if (![devEntry isKindOfClass:[NSString class]]) return nil;
if ([devEntry isEqualToString:device])
return device;
}
}
return nil;
}
static BOOL Trash(NSString *path) {
BOOL result = NO;
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_8
if (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_8) {
result = [[NSFileManager defaultManager] trashItemAtURL:[NSURL fileURLWithPath:path] resultingItemURL:NULL error:NULL];
}
#endif
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_11
if (!result) {
result = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation
source:[path stringByDeletingLastPathComponent]
destination:@""
files:[NSArray arrayWithObject:[path lastPathComponent]]
tag:NULL];
}
#endif
// As a last resort try trashing with AppleScript.
// This allows us to trash the app in macOS Sierra even when the app is running inside
// an app translocation image.
if (!result) {
NSAppleScript *appleScript = [[[NSAppleScript alloc] initWithSource:
[NSString stringWithFormat:@"\
set theFile to POSIX file \"%@\" \n\
tell application \"Finder\" \n\
move theFile to trash \n\
end tell", path]] autorelease];
NSDictionary *errorDict = nil;
NSAppleEventDescriptor *scriptResult = [appleScript executeAndReturnError:&errorDict];
if (scriptResult == nil) {
NSLog(@"Trash AppleScript error: %@", errorDict);
}
result = (scriptResult != nil);
}
if (!result) {
NSLog(@"ERROR -- Could not trash '%@'", path);
}
return result;
}
static BOOL DeleteOrTrash(NSString *path) {
NSError *error;
if ([[NSFileManager defaultManager] removeItemAtPath:path error:&error]) {
return YES;
}
else {
// Don't log warning if on Sierra and running inside App Translocation path
if ([path rangeOfString:@"/AppTranslocation/"].location == NSNotFound)
NSLog(@"WARNING -- Could not delete '%@': %@", path, [error localizedDescription]);
return Trash(path);
}
}
static BOOL AuthorizedInstall(NSString *srcPath, NSString *dstPath, BOOL *canceled) {
if (canceled) *canceled = NO;
// Make sure that the destination path is an app bundle. We're essentially running 'sudo rm -rf'
// so we really don't want to fuck this up.
if (![[dstPath pathExtension] isEqualToString:@"app"]) return NO;
// Do some more checks
if ([[dstPath stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) return NO;
if ([[srcPath stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) return NO;
int pid, status;
AuthorizationRef myAuthorizationRef;
// Get the authorization
OSStatus err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &myAuthorizationRef);
if (err != errAuthorizationSuccess) return NO;
AuthorizationItem myItems = {kAuthorizationRightExecute, 0, NULL, 0};
AuthorizationRights myRights = {1, &myItems};
AuthorizationFlags myFlags = (AuthorizationFlags)(kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights | kAuthorizationFlagPreAuthorize);
err = AuthorizationCopyRights(myAuthorizationRef, &myRights, NULL, myFlags, NULL);
if (err != errAuthorizationSuccess) {
if (err == errAuthorizationCanceled && canceled)
*canceled = YES;
goto fail;
}
static OSStatus (*security_AuthorizationExecuteWithPrivileges)(AuthorizationRef authorization, const char *pathToTool,
AuthorizationFlags options, char * const *arguments,
FILE **communicationsPipe) = NULL;
if (!security_AuthorizationExecuteWithPrivileges) {
// On 10.7, AuthorizationExecuteWithPrivileges is deprecated. We want to still use it since there's no
// good alternative (without requiring code signing). We'll look up the function through dyld and fail
// if it is no longer accessible. If Apple removes the function entirely this will fail gracefully. If
// they keep the function and throw some sort of exception, this won't fail gracefully, but that's a
// risk we'll have to take for now.
security_AuthorizationExecuteWithPrivileges = (OSStatus (*)(AuthorizationRef, const char*,
AuthorizationFlags, char* const*,
FILE **)) dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges");
}
if (!security_AuthorizationExecuteWithPrivileges) goto fail;
// Delete the destination
{
char *args[] = {"-rf", (char *)[dstPath fileSystemRepresentation], NULL};
err = security_AuthorizationExecuteWithPrivileges(myAuthorizationRef, "/bin/rm", kAuthorizationFlagDefaults, args, NULL);
if (err != errAuthorizationSuccess) goto fail;
// Wait until it's done
pid = wait(&status);
if (pid == -1 || !WIFEXITED(status)) goto fail; // We don't care about exit status as the destination most likely does not exist
}
// Copy
{
char *args[] = {"-pR", (char *)[srcPath fileSystemRepresentation], (char *)[dstPath fileSystemRepresentation], NULL};
err = security_AuthorizationExecuteWithPrivileges(myAuthorizationRef, "/bin/cp", kAuthorizationFlagDefaults, args, NULL);
if (err != errAuthorizationSuccess) goto fail;
// Wait until it's done
pid = wait(&status);
if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status)) goto fail;
}
AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults);
return YES;
fail:
AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults);
return NO;
}
static BOOL CopyBundle(NSString *srcPath, NSString *dstPath) {
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
if ([fm copyItemAtPath:srcPath toPath:dstPath error:&error]) {
return YES;
}
else {
NSLog(@"ERROR -- Could not copy '%@' to '%@' (%@)", srcPath, dstPath, error);
return NO;
}
}
static NSString *ShellQuotedString(NSString *string) {
return [NSString stringWithFormat:@"'%@'", [string stringByReplacingOccurrencesOfString:@"'" withString:@"'\\''"]];
}
static void Relaunch(NSString *destinationPath) {
// The shell script waits until the original app process terminates.
// This is done so that the relaunched app opens as the front-most app.
int pid = [[NSProcessInfo processInfo] processIdentifier];
// Command run just before running open /final/path
NSString *preOpenCmd = @"";
NSString *quotedDestinationPath = ShellQuotedString(destinationPath);
// OS X >=10.5:
// Before we launch the new app, clear xattr:com.apple.quarantine to avoid
// duplicate "scary file from the internet" dialog.
if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5) {
// Add the -r flag on 10.6
preOpenCmd = [NSString stringWithFormat:@"/usr/bin/xattr -d -r com.apple.quarantine %@", quotedDestinationPath];
}
else {
preOpenCmd = [NSString stringWithFormat:@"/usr/bin/xattr -d com.apple.quarantine %@", quotedDestinationPath];
}
NSString *script = [NSString stringWithFormat:@"(while /bin/kill -0 %d >&/dev/null; do /bin/sleep 0.1; done; %@; /usr/bin/open %@) &", pid, preOpenCmd, quotedDestinationPath];
[NSTask launchedTaskWithLaunchPath:@"/bin/sh" arguments:[NSArray arrayWithObjects:@"-c", script, nil]];
}
+206
View File
@@ -0,0 +1,206 @@
LetsMove
========
A sample project that demonstrates how to move a running Mac OS X application to the Applications folder.
![Screenshot](http://i.imgur.com/euTRZiI.png)
Requirements
------------
Builds and runs on Mac OS X 10.6 or higher. Does NOT support sandboxed applications.
Usage
-----
Option 1:
Build then embed LetsMove.framework into your app.
Option 2:
Copy the following files into your project:
- PFMoveApplication.h
- PFMoveApplication.m
If your project has ARC enabled, you'll want to disable ARC on the above files. You can do so by adding -fno-objc-arc compiler flag to your PFMoveApplication.m source file. See http://stackoverflow.com/questions/6646052/how-can-i-disable-arc-for-a-single-file-in-a-project/6658549#6658549
If your application is localized, also copy the 'MoveApplication.string' files into your project.
Link your application against Security.framework.
In your app delegate's "-[applicationWillFinishLaunching:]" method, call the PFMoveToApplicationsFolderIfNecessary function at the very top.
License
-------
Public domain
Version History
---------------
* 1.24
- Add PFMoveIsInProgress function
- Update project for Xcode 9.1
* 1.23
- Fix localization not being used when built as a framework
- Fix backwards compatibility with OS X 10.9 and earlier being broken
- Make usable for Electron based apps or other apps that do not have access to the main thread dispatch queue
- Update Russian localization
* 1.22
- Fix not deleting or trashing itself after copying to /Applications in macOS Sierra
* 1.21
- Support for [Carthage](https://github.com/Carthage/Carthage) added
- Project now support OS X 10.6 and higher
* 1.20
- Support for applications bundled inside another application
- Brazilian Portuguese localization slightly updated
- Build warnings fixed
* 1.19
- Slovak localization added
* 1.18
- Catalan localization added
* 1.17
- Tranditional Chinese localization added.
* 1.16
- Deprecation warning that appears when minimum deployment target is set to OS X 10.10 taken care of
* 1.15
- Swedish localization added
* 1.14
- Hugarian, Serbian, and Turkish localizations added
- Macedonian localization added
* 1.13
- Polish localization added
* 1.12
- Use country code based .lproj directories
- Make it compile for projects that don't use precompiled headers to import AppKit.framework
- Minor adjustment to Dutch localization
- Warning fixes in example project
* 1.11
- Objective-C++ compatibility
* 1.10
- Fixed deprecation warnings that show up when building against the OS X 10.9 SDK.
* 1.9
- Removed OS X 10.4 support
- Properly detect if the running app is in a disk image
- Fixed a bug where if the app's name contained a quote, the app could not be moved
- After a successful move, delete the application instead of moving it to the Trash.
- Other fixes and improvements
* 1.8
- If the app is already there in the Applications folder but not writable, request authentication from user
- Added Korean localization
* 1.7.2
- Fixed an exception that could happen.
* 1.7.1
- Refactoring
* 1.7
- Only move to ~/Appilcations directory if an app is already in there.
* 1.6.3
- Function calls deprecated in 10.7 no longer cause compile time warnings.
- Added Simplified Chinese and European Portuguese localizations
* 1.6.2
- Garbage collection compatibility added
- Use a new method to check if an application is already running on Mac OS X 10.6 systems or higher
* 1.6.1
- Use exit(0) to terminate the app before relaunching instead of [NSApp terminate:]. We don't want applicationShouldTerminate or applicationWillTerminate NSApplication delegate methods to be called, possibly introducing side effects.
* 1.6
- Resolve any aliases when finding the Applications directory
* 1.5.2
- Cleaned up the code a bit. Almost functionally equivalent to 1.5.1.
* 1.5.1
- Fixed a bug with clearing the quarantine file attribute on Mac OS X 10.5
* 1.5
- Don't prompt to move the application if it has "Applications" in its path somewhere
* 1.4
- Mac OS X 10.5 compatibility fixes
* 1.3
- Fixed a rare bug in the shell script that checks to see if the app is already running
- Clear quarantine flag after copying
- Compile time option to show normal sized alert supress checkbox button
- German, Danish, and Norwegian localizations added
* 1.2
- Copy application from disk image then unmount disk image
- Spanish, French, Dutch, and Russian localizations
* 1.1
- Prefers ~/Applications over /Applications if it exists
- Escape key pushes the "Do Not Move" button
* 1.0
- First release
Code Contributors:
-------------
* Andy Kim
* John Brayton
* Chad Sellers
* Kevin LaCoste
* Rasmus Andersson
* Timothy J. Wood
* Matt Gallagher
* Whitney Young
* Nick Moore
* Nicholas Riley
* Matt Prowse
* Maxim Ananov
* Charlie Stigler
Translators:
------------
* Eita Hayashi (Japanese)
* Gleb M. Borisov, Maxim Ananov (Russian)
* Wouter Broekhof (Dutch)
* Rasmus Andersson / Spotify (French and Spanish)
* Markus Kirschner (German)
* Fredrik Nannestad (Danish)
* Georg Alexander Bøe (Norwegian)
* Marco Improda (Italian)
* Venj Chu (Simplified Chinese)
* Sérgio Miranda (European Portuguese)
* Victor Figueiredo and BR Lingo (Brazilian Portuguese)
* AppLingua (Korean)
* Czech X Team (Czech)
* Marek Telecki (Polish)
* Petar Vlahu (Macedonian)
* Václav Slavík (Hungarian, Serbian, and Turkish)
* Erik Vikström (Swedish)
* Inndy Lin (Traditional Chinese)
* aONe (Catalan)
* Marek Hrusovsky (Slovak)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
/* Localized versions of Info.plist keys */
+4208
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 1675 files, more files were not shown because too many files have changed in this diff. Show more