├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md ├── pull_request_template.md └── workflows │ ├── ci-macos.yml │ └── markdown-link-check.yml ├── .gitignore ├── .swiftlint.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Simctl │ └── SimctlClient.swift ├── SimctlCLI │ ├── Commands.swift │ ├── ListDevices.swift │ ├── SimctlServer.swift │ ├── StartServer.swift │ ├── Swifter+Extensions.swift │ └── main.swift └── SimctlShared │ └── SimctlShared.swift ├── bin └── SimctlCLI ├── docs ├── Overview.png ├── Overview.sketch ├── SimctlExample.gif └── XcodeSwiftPackage.png └── renovate.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ctreffs] 2 | custom: ['https://www.paypal.com/donate?hosted_button_id=GCG3K54SKRALQ'] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Something isn't working as expected, create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | 17 | 18 | ### Bug Description 19 | 20 | *A clear and concise description of what the bug is. 21 | Replace this paragraph with a short description of the incorrect behavior. 22 | (If this is a regression, please note the last version of the package that exhibited the correct behavior in addition to your current version.)* 23 | 24 | ### Information 25 | 26 | - **Package version:** What tag or branch of this package are you using? e.g. tag `1.2.3` or branch `main` 27 | - **Platform version:** Please tell us the version number of your operating system. e.g. `macOS 11.2.3` or `Ubuntu 20.04` 28 | - **Swift version:** Paste the output of `swift --version` here. 29 | 30 | ### Checklist 31 | 32 | - [ ] If possible, I've reproduced the issue using the `main`/`master` branch of this package. 33 | - [ ] I've searched for existing issues under the issues tab. 34 | - [ ] The bug is reproducible 35 | 36 | ### Steps to Reproduce 37 | 38 | *Steps to reproduce the behavior:* 39 | 40 | 1. Go to '...' 41 | 2. '....' 42 | 43 | *Replace this paragraph with an explanation of how to reproduce the incorrect behavior. 44 | Include a simple code example, if possible.* 45 | 46 | ### Expected behavior 47 | 48 | *A clear and concise description of what you expected to happen. 49 | Describe what you expect to happen.* 50 | 51 | ### Actual behavior 52 | 53 | *Describe or copy/paste the behavior you observe.* 54 | 55 | ### Screenshots 56 | 57 | If applicable, add screenshots to help explain your problem. 58 | 59 | ### Additional context 60 | 61 | *Add any other context about the problem here.* 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: A suggestion for a new feature or idea for this project 4 | labels: enhancement 5 | --- 6 | 7 | 13 | 14 | ### Feature request 15 | 16 | *Replace this paragraph with a description of your proposed feature. 17 | A clear and concise description of what the idea or problem is you want to solve. 18 | Please be sure to describe some concrete use cases for the new feature -- be as specific as possible. 19 | Provide links to existing issues or external references/discussions, if appropriate.* 20 | 21 | ### Describe the solution you'd like 22 | 23 | *A clear and concise description of what you want to happen.* 24 | 25 | ### Describe alternatives you've considered 26 | 27 | *A clear and concise description of any alternative solutions or features you've considered.* 28 | 29 | ### Additional context 30 | 31 | *Add any other context or screenshots about the feature request here.* 32 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ### Description 13 | 14 | *Replace this paragraph with a description of your changes and rationale. 15 | Provide links to an existing issue or external references/discussions, if appropriate.* 16 | 17 | ### Detailed Design 18 | 19 | *Include any additional information about the design here. At minimum, describe a synopsis of any public API additions.* 20 | 21 | ```swift 22 | /// The new feature implemented by this pull request. 23 | public struct Example: Collection { 24 | } 25 | ``` 26 | 27 | ### Documentation 28 | 29 | *How has the new feature been documented? 30 | Have the relevant portions of the guides in the Documentation folder been updated in addition to symbol-level documentation?* 31 | 32 | ### Testing 33 | 34 | *How is the new feature tested? 35 | Please ensure CI is not broken* 36 | 37 | ### Performance 38 | 39 | *How did you verify the new feature performs as expected?* 40 | 41 | ### Source Impact 42 | 43 | *What is the impact of this change on existing users of this package? Does it deprecate or remove any existing API?* 44 | 45 | ### Checklist 46 | 47 | - [ ] I've read the [Contribution Guidelines](https://github.com/ctreffs/SwiftSimctl/blob/master/CONTRIBUTING.md) 48 | - [ ] I've followed the coding style of the rest of the project. 49 | - [ ] I've added tests covering all new code paths my change adds to the project (to the extent possible). 50 | - [ ] I've added benchmarks covering new functionality (if appropriate). 51 | - [ ] I've verified that my change does not break any existing tests or introduce unexpected benchmark regressions. 52 | - [ ] I've updated the documentation (if appropriate). 53 | -------------------------------------------------------------------------------- /.github/workflows/ci-macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | macos-build-release: 12 | runs-on: macOS-latest 13 | strategy: 14 | matrix: 15 | xcode: ["14.2", "13.4.1"] 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@master 19 | 20 | - name: Select Xcode ${{ matrix.xcode }} 21 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 22 | 23 | - name: Build Release 24 | run: make buildRelease 25 | env: 26 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer 27 | 28 | - name: Build SimctlCLI Release 29 | run: make buildSimctlCLI 30 | env: 31 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer 32 | 33 | - name: Upload build artifacts on failure 34 | if: failure() 35 | uses: actions/upload-artifact@v3.1.2 36 | with: 37 | name: build-artifacts-${{ matrix.xcode }}-${{ github.run_id }} 38 | path: | 39 | *.lcov 40 | .build/*.yaml 41 | .build/**/*.a 42 | .build/**/*.so 43 | .build/**/*.dylib 44 | .build/**/*.dSYM 45 | .build/**/*.json 46 | -------------------------------------------------------------------------------- /.github/workflows/markdown-link-check.yml: -------------------------------------------------------------------------------- 1 | name: Check markdown links 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | markdown-link-check: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@master 16 | - name: markdown-link-check 17 | uses: gaurav-nelson/github-action-markdown-link-check@master 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/xcshareddata/WorkspaceSettings.xcsettings 2 | *.dSYM 3 | *.dSYM.zip 4 | *.hmap 5 | *.ipa 6 | *.mode1v3 7 | *.mode2v3 8 | *.moved-aside 9 | *.pbxuser 10 | *.perspectivev3 11 | *.xccheckout 12 | *.xcodeproj/* 13 | *.xcscmblueprint 14 | *.xcworkspace 15 | ._* 16 | .accio/ 17 | .apdisk 18 | .AppleDB 19 | .AppleDesktop 20 | .AppleDouble 21 | .build/ 22 | .com.apple.timemachine.donotpresent 23 | .DocumentRevisions-V100 24 | .DS_Store 25 | .fseventsd 26 | .LSOverride 27 | .Spotlight-V100 28 | .swiftpm/xcode 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | /*.gcno 33 | build/ 34 | Carthage/Build 35 | Carthage/Checkouts 36 | Dependencies/ 37 | DerivedData/ 38 | fastlane/Preview.html 39 | fastlane/report.xml 40 | fastlane/screenshots/**/*.png 41 | fastlane/test_output 42 | Icon 43 | iOSInjectionProject/ 44 | Network Trash Folder 45 | Packages/ 46 | playground.xcworkspace 47 | Temporary Items 48 | timeline.xctimeline 49 | xcuserdata/ -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests 4 | excluded: 5 | - docs 6 | - build 7 | - .build 8 | - Tests/*/XCTestManifests.swift 9 | - Tests/*/*/XCTestManifests.swift 10 | - Tests/LinuxMain.swift 11 | - Sources/Demos/*/main.swift 12 | identifier_name: 13 | excluded: 14 | - as 15 | - dt 16 | - dx 17 | - dy 18 | - dz 19 | - i 20 | - id 21 | - j 22 | - s 23 | - t 24 | - to 25 | - u 26 | - up 27 | - v 28 | - w 29 | - x 30 | - y 31 | - z 32 | line_length: 220 33 | number_separator: 34 | minimum_length: 5 35 | opt_in_rules: 36 | #- anyobject_protocol 37 | #- explicit_acl 38 | #- explicit_enum_raw_value 39 | #- explicit_type_interface 40 | #- extension_access_modifier 41 | #- file_header 42 | #- file_name 43 | #- missing_docs 44 | #- multiline_arguments_brackets 45 | #- no_grouping_extension 46 | #- multiline_literal_brackets 47 | - array_init 48 | - attributes 49 | - closure_body_length 50 | - closure_end_indentation 51 | - closure_spacing 52 | - collection_alignment 53 | - conditional_returns_on_newline 54 | - contains_over_first_not_nil 55 | - convenience_type 56 | - custom_rules 57 | - discouraged_object_literal 58 | - discouraged_optional_boolean 59 | - discouraged_optional_collection 60 | - empty_count 61 | - empty_string 62 | - empty_xctest_method 63 | - explicit_init 64 | - explicit_self 65 | - explicit_top_level_acl 66 | - fallthrough 67 | - fatal_error_message 68 | - first_where 69 | - force_unwrapping 70 | - function_default_parameter_at_end 71 | - identical_operands 72 | - implicit_return 73 | - implicitly_unwrapped_optional 74 | - joined_default_parameter 75 | - legacy_random 76 | - let_var_whitespace 77 | - literal_expression_end_indentation 78 | - lower_acl_than_parent 79 | - modifier_order 80 | - multiline_arguments 81 | - multiline_function_chains 82 | - multiline_parameters 83 | #- multiline_parameters_brackets 84 | - nimble_operator 85 | - no_extension_access_modifier 86 | - number_separator 87 | - object_literal 88 | - operator_usage_whitespace 89 | - overridden_super_call 90 | - override_in_extension 91 | - pattern_matching_keywords 92 | - prefixed_toplevel_constant 93 | - private_action 94 | - private_outlet 95 | - prohibited_interface_builder 96 | - prohibited_super_call 97 | - quick_discouraged_call 98 | - quick_discouraged_focused_test 99 | - quick_discouraged_pending_test 100 | - redundant_nil_coalescing 101 | - redundant_type_annotation 102 | - required_enum_case 103 | - single_test_class 104 | - sorted_first_last 105 | - sorted_imports 106 | - static_operator 107 | - strict_fileprivate 108 | - switch_case_on_newline 109 | - toggle_bool 110 | - trailing_closure 111 | #- unavailable_function 112 | - unneeded_parentheses_in_closure_argument 113 | - untyped_error_in_catch 114 | - unused_import 115 | - vertical_parameter_alignment_on_call 116 | - vertical_whitespace_between_cases 117 | - vertical_whitespace_closing_braces 118 | - vertical_whitespace_opening_braces 119 | - yoda_condition -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is a list of the people responsible for ensuring that contributions 2 | # to this projected are reviewed, either by themselves or by someone else. 3 | # They are also the gatekeepers for their part of this project, with the final 4 | # word on what goes in or not. 5 | # The code owners file uses a .gitignore-like syntax to specify which parts of 6 | # the codebase is associated with an owner. See 7 | # 8 | # for details. 9 | # The following lines are used by GitHub to automatically recommend reviewers. 10 | # Each line is a file pattern followed by one or more owners. 11 | 12 | * @ctreffs 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement 63 | e.g. via [content abuse report][ref-report-abuse]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][ref-homepage-cc], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | . 126 | Translations are available at 127 | . 128 | 129 | 130 | 131 | [ref-homepage-cc]: https://www.contributor-covenant.org 132 | [ref-report-abuse]: https://docs.github.com/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam#reporting-an-issue-or-pull-request 133 | [ref-gh-coc]: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project 134 | [ref-gh-abuse]: https://docs.github.com/en/communities/moderating-comments-and-conversations/managing-how-contributors-report-abuse-in-your-organizations-repository 135 | [ref-coc-guide]: https://opensource.guide/code-of-conduct/ 136 | 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 💁 Contributing to this project 2 | 3 | 4 | > First off, thank you for considering contributing to this project. 5 | > It’s [people like you][ref-contributors] that keep this project alive and make it great! 6 | > Thank you! 🙏💜🎉👍 7 | 8 | The following is a set of **guidelines for contributing** to this project. 9 | Use your best judgment and feel free to propose changes to this document in a pull request. 10 | 11 | **Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) 12 | 13 | ### 💡 Your contribution - the sky is the limit 🌈 14 | 15 | This is an open source project and we love to receive contributions from our community — [**you**][ref-contributors]! 16 | 17 | There are many ways to contribute, from writing __tutorials__ or __blog posts__, improving the [__documentation__][ref-documentation], submitting [__bug reports__][ref-issues-new] and [__enhancement__][ref-pull-request-new] or 18 | [__writing code__][ref-pull-request-new] which can be incorporated into the repository itself. 19 | 20 | When contributing to this project, please feel free to discuss the changes and ideas you wish to contribute with the repository owners before making a change by opening a [new issue][ref-issues-new] and add the **feature request** tag to that issue. 21 | 22 | Note that we have a [code of conduct][ref-code-of-conduct], please follow it in all your interactions with the project. 23 | 24 | ### 🐞 You want to report a bug or file an issue? 25 | 26 | 1. Ensure that it was **not already reported** and is being worked on by checking [open issues][ref-issues]. 27 | 2. Create a [new issue][ref-issues-new] with a **clear and descriptive title** 28 | 3. Write a **detailed comment** with as much relevant information as possible including 29 | - *how to reproduce* the bug 30 | - a *code sample* or an *executable test case* demonstrating the expected behavior that is not occurring 31 | - any *files that could help* trace it down (i.e. logs) 32 | 33 | ### 🩹 You wrote a patch that fixes an issue? 34 | 35 | 1. Open a [new pull request (PR)][ref-pull-request-new] with the patch. 36 | 2. Ensure the PR description clearly describes the problem and solution. 37 | 3. Link the relevant **issue** if applicable ([how to link issues in PRs][ref-pull-request-how-to]). 38 | 4. Ensure that [**no tests are failing**][ref-gh-actions] and **coding conventions** are met 39 | 5. Submit the patch and await review. 40 | 41 | ### 🎁 You want to suggest or contribute a new feature? 42 | 43 | That's great, thank you! You rock 🤘 44 | 45 | If you want to dive deep and help out with development on this project, then first get the project [installed locally][ref-readme]. 46 | After that is done we suggest you have a look at tickets in our [issue tracker][ref-issues]. 47 | You can start by looking through the beginner or help-wanted issues: 48 | - [__Good first issues__][ref-issues-first] are issues which should only require a few lines of code, and a test or two. 49 | - [__Help wanted issues__][ref-issues-help] are issues which should be a bit more involved than beginner issues. 50 | These are meant to be a great way to get a smooth start and won't put you in front of the most complex parts of the system. 51 | 52 | If you are up to more challenging tasks with a bigger scope, then there are a set of tickets with a __feature__, __enhancement__ or __improvement__ tag. 53 | These tickets have a general overview and description of the work required to finish. 54 | If you want to start somewhere, this would be a good place to start. 55 | That said, these aren't necessarily the easiest tickets. 56 | 57 | For any new contributions please consider these guidelines: 58 | 59 | 1. Open a [new pull request (PR)][ref-pull-request-new] with a **clear and descriptive title** 60 | 2. Write a **detailed comment** with as much relevant information as possible including: 61 | - What your feature is intended to do? 62 | - How it can be used? 63 | - What alternatives where considered, if any? 64 | - Has this feature impact on performance or stability of the project? 65 | 66 | #### Your contribution responsibilities 67 | 68 | Don't be intimidated by these responsibilities, they are easy to meet if you take your time to develop your feature 😌 69 | 70 | - [x] Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. 71 | - [x] Ensure (cross-)platform compatibility for every change that's accepted. An addition should not reduce the number of platforms that the project supports. 72 | - [x] Ensure **coding conventions** are met. Lint your code with the project's default tools. Project wide commands are available through the [Makefile][ref-makefile] in the repository root. 73 | - [x] Add tests for your feature that prove it's working as expected. Code coverage should not drop below its previous value. 74 | - [x] Ensure none of the existing tests are failing after adding your changes. 75 | - [x] Document your public API code and ensure to add code comments where necessary. 76 | 77 | 78 | ### ⚙️ How to set up the environment 79 | 80 | Please consult the [README][ref-readme] for installation instructions. 81 | 82 | 83 | 84 | [ref-code-of-conduct]: https://github.com/ctreffs/SwiftSimctl/blob/master/CODE_OF_CONDUCT.md 85 | [ref-contributors]: https://github.com/ctreffs/SwiftSimctl/graphs/contributors 86 | [ref-documentation]: https://github.com/ctreffs/SwiftSimctl/wiki 87 | [ref-gh-actions]: https://github.com/ctreffs/SwiftSimctl/actions 88 | [ref-issues-first]: https://github.com/ctreffs/SwiftSimctl/issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue" 89 | [ref-issues-help]: https://github.com/ctreffs/SwiftSimctl/issues?q=is%3Aopen+is%3Aissue+label%3A"help+wanted" 90 | [ref-issues-new]: https://github.com/ctreffs/SwiftSimctl/issues/new/choose 91 | [ref-issues]: https://github.com/ctreffs/SwiftSimctl/issues 92 | [ref-pull-request-how-to]: https://docs.github.com/en/github/writing-on-github/autolinked-references-and-urls 93 | [ref-pull-request-new]: https://github.com/ctreffs/SwiftSimctl/compare 94 | [ref-readme]: https://github.com/ctreffs/SwiftSimctl/blob/master/README.md 95 | [ref-makefile]: https://github.com/ctreffs/SwiftSimctl/blob/master/Makefile 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Christian Treffs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINDIR_PREFIX?=/usr/local 2 | SIMCTLCLI_NAME = SimctlCLI 3 | 4 | .PHONY: lint-fix 5 | lint-fix: 6 | swiftlint --fix --format 7 | swiftlint lint --quiet 8 | 9 | .PHONY: buildRelease 10 | buildRelease: 11 | swift build -c release 12 | 13 | .PHONY: buildSimctlCLI 14 | buildSimctlCLI: 15 | @printf "Building SimctlCLI..." 16 | @swift build -Xswiftc -Osize -Xswiftc -whole-module-optimization -c release --product $(SIMCTLCLI_NAME) 17 | @cp "`swift build -c release --product $(SIMCTLCLI_NAME) --show-bin-path`/$(SIMCTLCLI_NAME)" ./bin 18 | @echo "Done" 19 | 20 | .PHONY: cleanBuildSimctlCLI 21 | cleanBuildSimctlCLI: cleanArtifacts buildSimctlCLI 22 | 23 | .PHONY: installSimctlCLI 24 | installSimctlCLI: buildSimctlCLI 25 | @mkdir -p $(BINDIR_PREFIX)/bin 26 | @install `swift build -c release --product $(SIMCTLCLI_NAME) --show-bin-path`/$(SIMCTLCLI_NAME) $(BINDIR_PREFIX)/bin 27 | @echo "Installed $(SIMCTLCLI_NAME) to $(BINDIR_PREFIX)/bin/$(SIMCTLCLI_NAME)" 28 | 29 | .PHONY: uninstallSimctlCLI 30 | uninstallSimctlCLI: 31 | @rm -f $(BINDIR_PREFIX)/bin/$(SIMCTLCLI_NAME) 32 | @echo "Removed $(BINDIR_PREFIX)/bin/$(SIMCTLCLI_NAME)" 33 | 34 | .PHONY: precommit 35 | precommit: lint-fix 36 | 37 | .PHONY: genLinuxTests 38 | genLinuxTests: 39 | swift test --generate-linuxmain 40 | swiftlint --fix --format --path Tests/ 41 | 42 | .PHONY: test 43 | test: genLinuxTests 44 | swift test 45 | 46 | .PHONY: genXcode 47 | genXcode: 48 | swift package generate-xcodeproj --enable-code-coverage --skip-extra-files 49 | 50 | .PHONY: clean 51 | clean: 52 | swift package reset 53 | rm -rdf .swiftpm/xcode 54 | rm -rdf .build/ 55 | rm Package.resolved 56 | rm .DS_Store 57 | 58 | .PHONY: cleanArtifacts 59 | cleanArtifacts: 60 | swift package clean 61 | 62 | # Test links in README 63 | # requires 64 | .PHONY: testReadme 65 | testReadme: 66 | markdown-link-check -p -v ./README.md 67 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ShellOut", 6 | "repositoryURL": "https://github.com/JohnSundell/ShellOut.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "e1577acf2b6e90086d01a6d5e2b8efdaae033568", 10 | "version": "2.3.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-argument-parser", 15 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 16 | "state": { 17 | "branch": null, 18 | "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca", 19 | "version": "0.5.0" 20 | } 21 | }, 22 | { 23 | "package": "Swifter", 24 | "repositoryURL": "https://github.com/httpswift/swifter.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "9483a5d459b45c3ffd059f7b55f9638e268632fd", 28 | "version": "1.5.0" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "SwiftSimctl", 6 | platforms: [ 7 | .iOS(.v11), 8 | .tvOS(.v11), 9 | .macOS(.v10_12) 10 | ], 11 | products: [ 12 | .executable(name: "SimctlCLI", targets: ["SimctlCLI"]), 13 | .library(name: "Simctl", targets: ["Simctl"]) 14 | ], 15 | dependencies: [ 16 | .package(name: "swift-argument-parser", url: "https://github.com/apple/swift-argument-parser", from: "1.2.2"), 17 | .package(name: "ShellOut", url: "https://github.com/JohnSundell/ShellOut.git", from: "2.3.0"), 18 | .package(name: "Swifter", url: "https://github.com/httpswift/swifter.git", from: "1.5.0") 19 | ], 20 | targets: [ 21 | .target(name: "SimctlShared"), 22 | .target(name: "SimctlCLI", dependencies: ["SimctlShared", "ShellOut", "Swifter", .product(name: "ArgumentParser", package: "swift-argument-parser")]), 23 | .target(name: "Simctl", dependencies: ["SimctlShared"]) 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Simctl 2 | 3 | [![macOS](https://github.com/ctreffs/SwiftSimctl/actions/workflows/ci-macos.yml/badge.svg)](https://github.com/ctreffs/SwiftSimctl/actions/workflows/ci-macos.yml) 4 | [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/ctreffs/SwiftSimctl/blob/master/LICENSE) 5 | [![swift-version-compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fctreffs%2FSwiftSimctl%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/ctreffs/SwiftSimctl) 6 | [![platform-compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fctreffs%2FSwiftSimctl%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/ctreffs/SwiftSimctl) 7 | 8 |

9 | simctl-example-gif 10 |

11 | 12 | 13 | This is a small tool (SimctlCLI) and library (Simctl), written in Swift, to automate [`xcrun simctl`](https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/iOS_Simulator_Guide/InteractingwiththeiOSSimulator/InteractingwiththeiOSSimulator.html#//apple_ref/doc/uid/TP40012848-CH3-SW4) commands for Simulator in unit and UI tests. 14 | 15 | It enables, among other things, reliable **fully automated** testing of Push Notifications with dynamic content, driven by a UI Test you control. 16 | 17 | ### 🚧 Architecture 18 | 19 |

20 | 21 |

22 | 23 | Swift Simctl is made of two parts. `SimctlCLI` and `Simctl`. 24 | 25 | `Simctl` is a Swift library that can be added to your project's test bundles. 26 | It provides an interface to commands that are otherwise only available via `xcrun simctl` from within your test code. 27 | To enable calling these commands `Simctl` communicates over a local network connection to `SimctlCLI`. 28 | 29 | `SimctlCLI` is a small command line tool that starts a local server, listens to requests from `Simctl` (the client library) and executes `xcrun simctl` commands. 30 | 31 | ### ⌨ Available Commands 32 | 33 | The following commands will be available in code in your (test) targets: 34 | 35 | - Send push notifications with custom payload 36 | - Grant or revoke privacy permissions (i.e. camera, photos ...) 37 | - Set the device UI appearance to light or dark mode 38 | - Set status bar overrides (i.e. data network, time ...) 39 | - Uninstall app by bundle id 40 | - Terminate app by bundle id 41 | - Rename device 42 | - Trigger iCloud Sync 43 | - Open URLs including registered URL schemes 44 | - Erase the contents and settings of the simulator 45 | - Get app container 46 | 47 | ## ❔ Why would you (not) use this 48 | 49 | #### ➕ Pro 50 | 51 | - Closed system (Mac with Xcode + Simulator) 52 | - No external dependencies on systems like [APNS](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html) 53 | - No custom test code bloating your code base (AppDelegate) unnecessarily 54 | - Push notifications can be simulated properly and the normal app cycle is preserved 55 | - Runs on CI machines 56 | - Your app stays a black box and does not need to be modified 57 | 58 | #### ➖ Contra 59 | 60 | - Needs a little configuration in your Xcode project 61 | - Only available for Xcode 11.4+ 62 | 63 | For specific usage please refer to the example projects **[Swift Simctl Package Example](https://github.com/ctreffs/SwiftSimctlExample)** 64 | 65 | ## 🚀 Getting Started 66 | 67 | These instructions will get your copy of the project up and running on your machine. 68 | 69 | ### 📋 Prerequisites 70 | 71 | - [Xcode 11.4](https://developer.apple.com/documentation/xcode_release_notes/) and higher. 72 | - [Swift Package Manager (SPM)](https://github.com/apple/swift-package-manager) 73 | 74 | ### 💻 Usage 75 | 76 | ### 📦 Swift Package 77 | 78 | To use Swift Simctl in your Xcode project add the package: 79 | 80 | 1. Xcode > File > Swift Packages > Add Package Dependency... 81 | 2. Choose Package Repository > Search: `SwiftSimctl` or find `https://github.com/ctreffs/SwiftSimctl.git` 82 | 3. Select `SwiftSimctl` package > `Next` ![xcode-swift-package](docs/XcodeSwiftPackage.png) 83 | 4. Do not forget to add the dependency to your (test) target 84 | 5. Use `import Simctl` to access the library in your (test) target. 85 | 86 | #### Running the server alongside your tests 87 | 88 | Make sure that for the duration of your test run `SimctlCLI` runs on your host machine. 89 | To automate that with Xcode itself use the following snippets as pre and post action of your test target. 90 | 91 | ###### `Your Scheme` > Test > Pre-Actions > Run Script 92 | 93 | ```sh 94 | #!/bin/bash 95 | killall SimctlCLI # cleaning up hanging servers 96 | set -e # fail fast 97 | # start the server non-blocking from the checked out package 98 | ${BUILD_ROOT}/../../SourcePackages/checkouts/SwiftSimctl/bin/SimctlCLI start-server > /dev/null 2>&1 & 99 | ``` 100 | 101 | ###### `Your Scheme` > Test > Post-Actions > Run Script 102 | 103 | ```sh 104 | #!/bin/bash 105 | set -e 106 | killall SimctlCLI 107 | 108 | ``` 109 | 110 | ###### 📝 Code Example Swift Package 111 | 112 | Please refer to the example project for an in depth code example **** 113 | 114 | ##### 💭 Port and settings 115 | 116 | The default port used by the server is `8080`. 117 | If you need to use another port you need to provide it via the `--port` flag when calling `SimctlCLI` and adjust 118 | the client port accordingly when setting up your test in code. 119 | Use `SimctlCLI --help` to get help regarding this and other server configuration settings. 120 | 121 | 122 | ## 🙏 Kudos 123 | 124 | Swift Simctl would not be possible without these awesome libraries: 125 | 126 | - [ShellOut](https://github.com/JohnSundell/ShellOut) - easy command line invocations 127 | - [Swifter](https://github.com/httpswift/swifter) - a tiny http server 128 | 129 | ## 💁 How to contribute 130 | 131 | If you want to contribute please see the [CONTRIBUTION GUIDE](CONTRIBUTING.md) first. 132 | 133 | Before commiting code please ensure to run: 134 | 135 | - `make precommit` 136 | 137 | This project is currently maintained by [@ctreffs](https://github.com/ctreffs). 138 | See also the list of [contributors](https://github.com/ctreffs/SwiftSimctl/contributors) who participated in this project. 139 | 140 | ## 🔏 Licenses 141 | 142 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/ctreffs/SwiftSimctl/blob/master/LICENSE) file for details. 143 | -------------------------------------------------------------------------------- /Sources/Simctl/SimctlClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimctlClient.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 18.03.20. 6 | // 7 | 8 | import struct Foundation.UUID 9 | import struct Foundation.Data 10 | import class Foundation.URLSession 11 | import SimctlShared 12 | #if canImport(UIKit) 13 | import UIKit 14 | #elseif canImport(AppKit) 15 | import AppKit 16 | #else 17 | #error("Unsupported platform!") 18 | #endif 19 | 20 | // swiftlint:disable file_length 21 | 22 | /// SimctlClient provides methods to trigger remote execution of simctl commands from your app on a local machine. 23 | /// This is acchieved by opening a client-server connection and sending requests to the server 24 | /// which in turn trigger execution of local commands on the server machine. 25 | public class SimctlClient { 26 | /// Address and port to the host machine. 27 | /// 28 | /// Note: if you like to use another port here, you need to provide it 29 | /// when starting up the server via the `--port` flag. 30 | static var host: Host = .localhost(port: 8080) 31 | 32 | let session: URLSession 33 | let env: SimctlClientEnvironment 34 | 35 | /// Start client in a simulator environment. 36 | /// - Parameter simEnv: The simulator environment configuration. 37 | public convenience init(_ simEnv: SimulatorEnvironment) { 38 | self.init(environment: simEnv) 39 | } 40 | 41 | /// Start client in a given environment. 42 | public init(environment: SimctlClientEnvironment) { 43 | session = URLSession(configuration: .default) 44 | Self.host = environment.host 45 | self.env = environment 46 | } 47 | 48 | /// Request a push notification to be send to this app. 49 | /// - Parameters: 50 | /// - notification: The notifcation payload to be send. 51 | /// - completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 52 | public func sendPushNotification(_ notification: PushNotificationContent, _ completion: @escaping DataTaskCallback) { 53 | dataTask(.sendPushNotification(env, notification)) { result in 54 | completion(result) 55 | } 56 | } 57 | 58 | /// Request a change in privacy settings for this app. 59 | /// - Parameters: 60 | /// - action: The privacy action to be taken 61 | /// - service: The service to be addressed. 62 | /// - completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 63 | public func setPrivacy(action: PrivacyAction, service: PrivacyService, _ completion: @escaping DataTaskCallback) { 64 | dataTask(.setPrivacy(env, action, service), completion) 65 | } 66 | 67 | /// Rename the current device to given name. 68 | /// - Parameters: 69 | /// - newName: The new name of the device. 70 | /// - completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 71 | public func renameDevice(to newName: String, _ completion: @escaping DataTaskCallback) { 72 | dataTask(.renameDevice(env, newName), completion) 73 | } 74 | 75 | /// Terminate the app with given app bundle identifier. 76 | /// - Parameters: 77 | /// - appBundleIdentifier: The bundle identifier of the app to terminate. 78 | /// - completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 79 | public func terminateApp(_ appBundleIdentifier: String, _ completion: @escaping DataTaskCallback) { 80 | dataTask(.terminateApp(env, appBundleIdentifier), completion) 81 | } 82 | 83 | /// Reset the contents and settings of the simulator 84 | /// - Parameters: 85 | /// - completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 86 | public func erase(_ completion: @escaping DataTaskCallback) { 87 | dataTask(.erase(env), completion) 88 | } 89 | 90 | /// Set the device UI appearance to given appearance 91 | /// - Parameters: 92 | /// - appearance: The appearance - currently light or dark. 93 | /// - completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 94 | public func setDeviceAppearance(_ appearance: DeviceAppearance, _ completion: @escaping DataTaskCallback) { 95 | dataTask(.setDeviceAppearance(env, appearance), completion) 96 | } 97 | 98 | /// Trigger iCloud sync on this device. 99 | /// - Parameter completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 100 | public func triggerICloudSync(_ completion: @escaping DataTaskCallback) { 101 | dataTask(.triggerICloudSync(env), completion) 102 | } 103 | 104 | /// Uninstall an app from this device. 105 | /// - Parameters: 106 | /// - appBundleIdentifier: The bundle identifier of the app to uninstall. 107 | /// - completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 108 | public func uninstallApp(_ appBundleIdentifier: String, _ completion: @escaping DataTaskCallback) { 109 | dataTask(.uninstallApp(env, appBundleIdentifier), completion) 110 | } 111 | 112 | /// Set status bar overrides for this device. 113 | /// - Parameters: 114 | /// - overrides: A set of status bar overrides. 115 | /// - completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 116 | public func setStatusBarOverrides(_ overrides: Set, _ completion: @escaping DataTaskCallback) { 117 | dataTask(.setStatusBarOverrides(env, overrides), completion) 118 | } 119 | 120 | /// Clear status bar overrides. 121 | /// - Parameter completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 122 | public func clearStatusBarOverrides(_ completion: @escaping DataTaskCallback) { 123 | dataTask(.clearStatusBarOverrides(env), completion) 124 | } 125 | 126 | /// Open a url. 127 | /// - Parameter url: URL to open. 128 | /// - Parameter completion: Result callback of the call. Use this to wait for an expectation to fulfill in a test case. 129 | public func openUrl(_ url: URL, completion: @escaping DataTaskCallback) { 130 | dataTask(.openURL(env, URLContainer(url: url)), completion) 131 | } 132 | 133 | public func getAppContainer(_ container: AppContainer? = nil, completion: @escaping DataTaskCallback) { 134 | dataTask(.getAppContainer(env, container), completion) 135 | } 136 | } 137 | 138 | // MARK: - Enviroment { 139 | public protocol SimctlClientEnvironment { 140 | var host: SimctlClient.Host { get } 141 | var bundleIdentifier: String? { get } 142 | var deviceUdid: UUID { get } 143 | } 144 | public struct SimulatorEnvironment: SimctlClientEnvironment { 145 | /// The host address and port of SimctlCLI server. 146 | public let host: SimctlClient.Host 147 | 148 | /// The bundle identifier of the app you want to address. 149 | public let bundleIdentifier: String? 150 | 151 | /// The Udid of the device or simulator you want to address. 152 | public let deviceUdid: UUID 153 | 154 | /// Initialize a simulator environment. 155 | /// - Parameters: 156 | /// - host: The host and port of the SimctlCLI server. 157 | /// - bundleIdentifier: The bundle identifier of the app you want to interact with. 158 | /// - deviceUdid: The Udid of the device you want to interact with. 159 | public init(host: SimctlClient.Host, bundleIdentifier: String?, deviceUdid: UUID) { 160 | self.host = host 161 | self.bundleIdentifier = bundleIdentifier 162 | self.deviceUdid = deviceUdid 163 | } 164 | 165 | /// Initialize a simulator environment. 166 | /// - Parameters: 167 | /// - host: The host and port of the SimctlCLI server. 168 | /// - bundle: Bundle of the app you want to interact with. 169 | /// - processInfo: The process info from where to get the device Udid. 170 | public init?(host: SimctlClient.Host, bundle: Bundle, processInfo: ProcessInfo) { 171 | guard let udid = Self.deviceId(processInfo) else { 172 | return nil 173 | } 174 | 175 | self.init(host: host, 176 | bundleIdentifier: bundle.bundleIdentifier, 177 | deviceUdid: udid) 178 | } 179 | 180 | /// Initialize a simulator environment. 181 | /// 182 | /// The device Udid of this device will be extracted from the process environment for you. 183 | /// 184 | /// - Parameters: 185 | /// - bundleIdentifier: The bundle identifier of the app you want to interact with. 186 | /// - host: The host and port of the SimctlCLI server. 187 | public init?(bundleIdentifier: String, host: SimctlClient.Host) { 188 | guard let udid = Self.deviceId(ProcessInfo()) else { 189 | return nil 190 | } 191 | self.init(host: host, bundleIdentifier: bundleIdentifier, deviceUdid: udid) 192 | } 193 | 194 | static func deviceId(_ processInfo: ProcessInfo) -> UUID? { 195 | guard let udidString = processInfo.environment[ProcessEnvironmentKey.simulatorUdid.rawValue] else { 196 | return nil 197 | } 198 | 199 | return UUID(uuidString: udidString) 200 | } 201 | } 202 | 203 | // MARK: - Process Info 204 | 205 | internal enum ProcessEnvironmentKey: String { 206 | case simulatorAudioDevicesPlistPath = "SIMULATOR_AUDIO_DEVICES_PLIST_PATH" 207 | case simulatorAudioSettingsPath = "SIMULATOR_AUDIO_SETTINGS_PATH" 208 | case simulatorBootTime = "SIMULATOR_BOOT_TIME" 209 | case simulatorCapabilities = "SIMULATOR_CAPABILITIES" 210 | case simulatorDeviceName = "SIMULATOR_DEVICE_NAME" 211 | case simulatorExtendedDisplayProperties = "SIMULATOR_EXTENDED_DISPLAY_PROPERTIES" 212 | case simulatorFramebufferFramework = "SIMULATOR_FRAMEBUFFER_FRAMEWORK" 213 | case simulatorHIDSystemManager = "SIMULATOR_HID_SYSTEM_MANAGER" 214 | case simulatorHostHome = "SIMULATOR_HOST_HOME" 215 | case simulatorLegacyAssetSuffic = "SIMULATOR_LEGACY_ASSET_SUFFIX" 216 | case simulatorLogRoot = "SIMULATOR_LOG_ROOT" 217 | case simulatorMainScreenHeight = "SIMULATOR_MAINSCREEN_HEIGHT" 218 | case simulatorMainScreenPitch = "SIMULATOR_MAINSCREEN_PITCH" 219 | case simulatorMainScreenScale = "SIMULATOR_MAINSCREEN_SCALE" 220 | case simulatorMainScreenWidth = "SIMULATOR_MAINSCREEN_WIDTH" 221 | case simulatorMemoryWarnings = "SIMULATOR_MEMORY_WARNINGS" 222 | case simulatorModelIdentifier = "SIMULATOR_MODEL_IDENTIFIER" 223 | case simulatorProductClass = "SIMULATOR_PRODUCT_CLASS" 224 | case simulatorRoot = "SIMULATOR_ROOT" 225 | case simulatorRuntimeBuildVersion = "SIMULATOR_RUNTIME_BUILD_VERSION" 226 | case simulatorRuntimeVersion = "SIMULATOR_RUNTIME_VERSION" 227 | case simulatorSharedResourcesDirectory = "SIMULATOR_SHARED_RESOURCES_DIRECTORY" 228 | case simulatorUdid = "SIMULATOR_UDID" 229 | case simulatorVersionInfo = "SIMULATOR_VERSION_INFO" 230 | } 231 | 232 | // MARK: - Host 233 | extension SimctlClient { 234 | public struct Host { 235 | let host: String 236 | 237 | public init(_ host: String) { 238 | self.host = host 239 | } 240 | } 241 | } 242 | extension SimctlClient.Host { 243 | public static func localhost(port: SimctlShared.Port) -> SimctlClient.Host { SimctlClient.Host("http://localhost:\(port)") } 244 | } 245 | extension SimctlClient.Host: Equatable { } 246 | 247 | // MARK: - Errors 248 | extension SimctlClient { 249 | public enum Error: Swift.Error { 250 | case noHttpResponse(Route) 251 | case unexpectedHttpStatusCode(Route, HTTPURLResponse) 252 | case noData(Route, HTTPURLResponse) 253 | case serviceError(Swift.Error) 254 | } 255 | } 256 | 257 | // MARK: - Routing 258 | extension SimctlClient { 259 | public enum Route { 260 | case sendPushNotification(SimctlClientEnvironment, PushNotificationContent) 261 | case setPrivacy(SimctlClientEnvironment, PrivacyAction, PrivacyService) 262 | case renameDevice(SimctlClientEnvironment, String) 263 | case terminateApp(SimctlClientEnvironment, String) 264 | case erase(SimctlClientEnvironment) 265 | case setDeviceAppearance(SimctlClientEnvironment, DeviceAppearance) 266 | case triggerICloudSync(SimctlClientEnvironment) 267 | case uninstallApp(SimctlClientEnvironment, String) 268 | case setStatusBarOverrides(SimctlClientEnvironment, Set) 269 | case clearStatusBarOverrides(SimctlClientEnvironment) 270 | case openURL(SimctlClientEnvironment, URLContainer) 271 | case getAppContainer(SimctlClientEnvironment, AppContainer?) 272 | 273 | @inlinable var httpMethod: HttpMethod { 274 | switch self { 275 | case .sendPushNotification, 276 | .setStatusBarOverrides, 277 | .openURL, 278 | .getAppContainer: 279 | return .post 280 | 281 | case .setPrivacy, 282 | .renameDevice, 283 | .terminateApp, 284 | .erase, 285 | .setDeviceAppearance, 286 | .triggerICloudSync, 287 | .uninstallApp, 288 | .clearStatusBarOverrides: 289 | return .get 290 | } 291 | } 292 | 293 | @inlinable var path: ServerPath { 294 | switch self { 295 | case .sendPushNotification: 296 | return .pushNotification 297 | 298 | case .setPrivacy: 299 | return .privacy 300 | 301 | case .renameDevice: 302 | return .renameDevice 303 | 304 | case .terminateApp: 305 | return .terminateApp 306 | 307 | case .erase: 308 | return .erase 309 | 310 | case .setDeviceAppearance: 311 | return .deviceAppearance 312 | 313 | case .triggerICloudSync: 314 | return .iCloudSync 315 | 316 | case .uninstallApp: 317 | return .uninstallApp 318 | 319 | case .setStatusBarOverrides: 320 | return .statusBarOverrides 321 | 322 | case .clearStatusBarOverrides: 323 | return .statusBarOverrides 324 | 325 | case .openURL: 326 | return .openURL 327 | 328 | case .getAppContainer: 329 | return .getAppContainer 330 | } 331 | } 332 | 333 | @inlinable var headerFields: [HeaderField] { 334 | func setEnv(_ env: SimctlClientEnvironment) -> [HeaderField] { 335 | var fields: [HeaderField] = [ 336 | .init(.deviceUdid, env.deviceUdid) 337 | ] 338 | if let bundleId = env.bundleIdentifier { 339 | fields.append(.init(.bundleIdentifier, bundleId)) 340 | } 341 | return fields 342 | } 343 | 344 | switch self { 345 | case let .sendPushNotification(env, _), 346 | let .erase(env), 347 | let .triggerICloudSync(env), 348 | let .setStatusBarOverrides(env, _), 349 | let .clearStatusBarOverrides(env), 350 | let .openURL(env, _), 351 | let .getAppContainer(env, _): 352 | return setEnv(env) 353 | 354 | case let .setPrivacy(env, action, service): 355 | var fields = setEnv(env) 356 | fields.append(HeaderField(.privacyAction, action.rawValue)) 357 | fields.append(HeaderField(.privacyService, service.rawValue)) 358 | return fields 359 | 360 | case let .renameDevice(env, name): 361 | var fields = setEnv(env) 362 | fields.append(HeaderField(.deviceName, name)) 363 | return fields 364 | 365 | case let .terminateApp(env, appBundleIdentifier), 366 | let .uninstallApp(env, appBundleIdentifier): 367 | var fields = setEnv(env) 368 | fields.append(HeaderField(.targetBundleIdentifier, appBundleIdentifier)) 369 | return fields 370 | 371 | case let .setDeviceAppearance(env, appearance): 372 | var fields = setEnv(env) 373 | fields.append(HeaderField(.deviceAppearance, appearance.rawValue)) 374 | return fields 375 | } 376 | } 377 | 378 | @inlinable var httpBody: Data? { 379 | let encoder = JSONEncoder() 380 | switch self { 381 | case let .sendPushNotification(_, notification): 382 | return try? encoder.encode(notification) 383 | 384 | case let .setStatusBarOverrides(_, overrides): 385 | return try? encoder.encode(overrides) 386 | 387 | case let .openURL(_, urlContainer): 388 | return try? encoder.encode(urlContainer) 389 | 390 | case let .getAppContainer(_, container): 391 | return try? encoder.encode(container) 392 | 393 | case .setPrivacy, 394 | .renameDevice, 395 | .terminateApp, 396 | .erase, 397 | .setDeviceAppearance, 398 | .triggerICloudSync, 399 | .uninstallApp, 400 | .clearStatusBarOverrides: 401 | return nil 402 | } 403 | } 404 | 405 | func asURL() -> URL { 406 | let urlString: String = SimctlClient.host.host + path.rawValue 407 | guard let url = URL(string: urlString) else { 408 | fatalError("no valid url \(urlString)") 409 | } 410 | 411 | return url 412 | } 413 | 414 | func asURLRequest() -> URLRequest { 415 | var request = URLRequest(url: asURL()) 416 | 417 | request.httpMethod = httpMethod.rawValue 418 | 419 | for field in headerFields { 420 | request.addValue(field.value, forHTTPHeaderField: field.headerField.rawValue) 421 | } 422 | 423 | request.httpBody = httpBody 424 | 425 | return request 426 | } 427 | } 428 | } 429 | 430 | // MARK: - Data tasks 431 | extension SimctlClient { 432 | public typealias DataTaskCallback = (Result) -> Void 433 | public typealias DecodedTaskCallback = (Result) -> Void where Value: Decodable 434 | 435 | func dataTaskDecoded(_ route: Route, _ completion: @escaping DecodedTaskCallback) where Value: Decodable { 436 | dataTask(route) { result in 437 | switch result { 438 | case let .failure(error): 439 | completion(.failure(error)) 440 | return 441 | 442 | case let .success(data): 443 | do { 444 | let decoder = JSONDecoder() 445 | let value: Value = try decoder.decode(Value.self, from: data) 446 | completion(.success(value)) 447 | return 448 | } catch { 449 | completion(.failure(error)) 450 | } 451 | } 452 | } 453 | } 454 | 455 | func dataTask(_ route: Route, _ completion: @escaping DataTaskCallback) { 456 | let task = session.dataTask(with: route.asURLRequest()) { data, urlResponse, error in 457 | if let error = error { 458 | completion(.failure(Error.serviceError(error))) 459 | return 460 | } 461 | 462 | guard let response = urlResponse as? HTTPURLResponse else { 463 | completion(.failure(Error.noHttpResponse(route))) 464 | return 465 | } 466 | 467 | guard response.statusCode == 200 else { 468 | completion(.failure(Error.unexpectedHttpStatusCode(route, response))) 469 | return 470 | } 471 | 472 | guard let data: Data = data else { 473 | completion(.failure(Error.noData(route, response))) 474 | return 475 | } 476 | 477 | completion(.success(data)) 478 | } 479 | task.resume() 480 | } 481 | } 482 | 483 | // MARK: - HTTP Methods 484 | @usableFromInline 485 | internal enum HttpMethod: String { 486 | case get = "GET" 487 | case post = "POST" 488 | case put = "PUT" 489 | } 490 | extension HttpMethod: Equatable { } 491 | 492 | // MARK: - Header field 493 | public struct HeaderField { 494 | let headerField: HeaderFieldKey 495 | let value: String 496 | 497 | public init(_ headerField: HeaderFieldKey, _ string: String) { 498 | self.headerField = headerField 499 | self.value = string 500 | } 501 | 502 | public init(_ headerField: HeaderFieldKey, _ bool: Bool) { 503 | self.headerField = headerField 504 | self.value = String(bool) 505 | } 506 | 507 | public init(_ headerField: HeaderFieldKey, _ int: Int) { 508 | self.headerField = headerField 509 | self.value = String(int) 510 | } 511 | 512 | public init(_ headerField: HeaderFieldKey, _ uuid: UUID) { 513 | self.headerField = headerField 514 | self.value = uuid.uuidString 515 | } 516 | } 517 | 518 | extension HeaderField: Equatable { } 519 | -------------------------------------------------------------------------------- /Sources/SimctlCLI/Commands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Commands.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 17.03.20. 6 | // 7 | 8 | import Foundation 9 | import ShellOut 10 | import SimctlShared 11 | 12 | extension ShellOutCommand { 13 | static func openSimulator() -> ShellOutCommand { 14 | .init(string: "open -b com.apple.iphonesimulator") 15 | } 16 | 17 | static func killAllSimulators() -> ShellOutCommand { 18 | .init(string: "killall Simulator") 19 | } 20 | 21 | private static func simctl(_ cmd: String) -> String { 22 | "xcrun simctl \(cmd)" 23 | } 24 | 25 | /// Usage: simctl list [-j | --json] [-v] [devices|devicetypes|runtimes|pairs] [|available] 26 | static func simctlList(_ filter: ListFilterType = .noFilter, _ asJson: Bool = false, _ verbose: Bool = false) -> ShellOutCommand { 27 | let cmd: String = [ 28 | "list", 29 | "\(asJson ? "--json" : "")", 30 | "\(verbose ? "-v" : "")", 31 | filter.rawValue 32 | ].joined(separator: " ") 33 | 34 | return .init(string: simctl(cmd)) 35 | } 36 | 37 | static func simctlBoot(device: UUID) -> ShellOutCommand { 38 | .init(string: simctl("boot \(device.uuidString)")) 39 | } 40 | 41 | static func simctlShutdown(device: UUID) -> ShellOutCommand { 42 | .init(string: simctl("shutdown \(device.uuidString)")) 43 | } 44 | 45 | static func simctlShutdownAllDevices() -> ShellOutCommand { 46 | .init(string: simctl("shutdown all")) 47 | } 48 | 49 | static func simctlOpen(url: URL, on device: UUID) -> ShellOutCommand { 50 | .init(string: simctl("openurl \(device.uuidString) \(url.absoluteString)")) 51 | } 52 | 53 | /// Usage: simctl ui