ominfra 0.0.0.dev154__py3-none-any.whl → 0.0.0.dev155__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. ominfra/manage/bootstrap.py +4 -0
  2. ominfra/manage/bootstrap_.py +5 -0
  3. ominfra/manage/commands/inject.py +8 -11
  4. ominfra/manage/commands/{execution.py → local.py} +1 -5
  5. ominfra/manage/commands/ping.py +23 -0
  6. ominfra/manage/commands/types.py +8 -0
  7. ominfra/manage/deploy/apps.py +72 -0
  8. ominfra/manage/deploy/config.py +8 -0
  9. ominfra/manage/deploy/git.py +136 -0
  10. ominfra/manage/deploy/inject.py +21 -0
  11. ominfra/manage/deploy/paths.py +81 -28
  12. ominfra/manage/deploy/types.py +13 -0
  13. ominfra/manage/deploy/venvs.py +66 -0
  14. ominfra/manage/inject.py +20 -4
  15. ominfra/manage/main.py +15 -27
  16. ominfra/manage/remote/_main.py +1 -1
  17. ominfra/manage/remote/config.py +0 -2
  18. ominfra/manage/remote/connection.py +7 -24
  19. ominfra/manage/remote/execution.py +1 -1
  20. ominfra/manage/remote/inject.py +3 -14
  21. ominfra/manage/system/commands.py +22 -2
  22. ominfra/manage/system/config.py +3 -1
  23. ominfra/manage/system/inject.py +16 -6
  24. ominfra/manage/system/packages.py +33 -7
  25. ominfra/manage/system/platforms.py +72 -0
  26. ominfra/manage/targets/__init__.py +0 -0
  27. ominfra/manage/targets/connection.py +150 -0
  28. ominfra/manage/targets/inject.py +42 -0
  29. ominfra/manage/targets/targets.py +87 -0
  30. ominfra/scripts/journald2aws.py +24 -7
  31. ominfra/scripts/manage.py +1880 -438
  32. ominfra/scripts/supervisor.py +187 -25
  33. ominfra/supervisor/configs.py +163 -18
  34. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/METADATA +3 -3
  35. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/RECORD +40 -29
  36. ominfra/manage/system/types.py +0 -5
  37. /ominfra/manage/{commands → deploy}/interp.py +0 -0
  38. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/LICENSE +0 -0
  39. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/WHEEL +0 -0
  40. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/entry_points.txt +0 -0
  41. {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev155.dist-info}/top_level.txt +0 -0
ominfra/scripts/manage.py CHANGED
@@ -92,9 +92,16 @@ CallableVersionOperator = ta.Callable[['Version', str], bool]
92
92
  CommandT = ta.TypeVar('CommandT', bound='Command')
93
93
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
94
94
 
95
+ # deploy/paths.py
96
+ DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
97
+ DeployPathSpec = ta.Literal['app', 'tag'] # ta.TypeAlias
98
+
95
99
  # ../../omlish/argparse/cli.py
96
100
  ArgparseCommandFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
97
101
 
102
+ # ../../omlish/lite/contextmanagers.py
103
+ ExitStackedT = ta.TypeVar('ExitStackedT', bound='ExitStacked')
104
+
98
105
  # ../../omlish/lite/inject.py
99
106
  U = ta.TypeVar('U')
100
107
  InjectorKeyCls = ta.Union[type, ta.NewType]
@@ -528,19 +535,28 @@ class MainConfig:
528
535
 
529
536
 
530
537
  ########################################
531
- # ../system/config.py
538
+ # ../deploy/config.py
532
539
 
533
540
 
534
541
  @dc.dataclass(frozen=True)
535
- class SystemConfig:
536
- platform: ta.Optional[str] = None
542
+ class DeployConfig:
543
+ deploy_home: ta.Optional[str] = None
537
544
 
538
545
 
539
546
  ########################################
540
- # ../system/types.py
547
+ # ../deploy/types.py
541
548
 
542
549
 
543
- SystemPlatform = ta.NewType('SystemPlatform', str)
550
+ DeployHome = ta.NewType('DeployHome', str)
551
+
552
+ DeployApp = ta.NewType('DeployApp', str)
553
+ DeployTag = ta.NewType('DeployTag', str)
554
+ DeployRev = ta.NewType('DeployRev', str)
555
+
556
+
557
+ class DeployAppTag(ta.NamedTuple):
558
+ app: DeployApp
559
+ tag: DeployTag
544
560
 
545
561
 
546
562
  ########################################
@@ -1221,8 +1237,6 @@ def async_cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
1221
1237
  """
1222
1238
  TODO:
1223
1239
  - def maybe(v: lang.Maybe[T])
1224
- - patch / override lite.check ?
1225
- - checker interface?
1226
1240
  """
1227
1241
 
1228
1242
 
@@ -1937,6 +1951,489 @@ def set_process_deathsig(sig: int) -> bool:
1937
1951
  return False
1938
1952
 
1939
1953
 
1954
+ ########################################
1955
+ # ../../../omlish/os/linux.py
1956
+ """
1957
+ ➜ ~ cat /etc/os-release
1958
+ NAME="Amazon Linux"
1959
+ VERSION="2"
1960
+ ID="amzn"
1961
+ ID_LIKE="centos rhel fedora"
1962
+ VERSION_ID="2"
1963
+ PRETTY_NAME="Amazon Linux 2"
1964
+
1965
+ ➜ ~ cat /etc/os-release
1966
+ PRETTY_NAME="Ubuntu 22.04.5 LTS"
1967
+ NAME="Ubuntu"
1968
+ VERSION_ID="22.04"
1969
+ VERSION="22.04.5 LTS (Jammy Jellyfish)"
1970
+ VERSION_CODENAME=jammy
1971
+ ID=ubuntu
1972
+ ID_LIKE=debian
1973
+ UBUNTU_CODENAME=jammy
1974
+
1975
+ ➜ omlish git:(master) docker run -i python:3.12 cat /etc/os-release
1976
+ PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
1977
+ NAME="Debian GNU/Linux"
1978
+ VERSION_ID="12"
1979
+ VERSION="12 (bookworm)"
1980
+ VERSION_CODENAME=bookworm
1981
+ ID=debian
1982
+ """
1983
+
1984
+
1985
+ @dc.dataclass(frozen=True)
1986
+ class LinuxOsRelease:
1987
+ """
1988
+ https://man7.org/linux/man-pages/man5/os-release.5.html
1989
+ """
1990
+
1991
+ raw: ta.Mapping[str, str]
1992
+
1993
+ # General information identifying the operating system
1994
+
1995
+ @property
1996
+ def name(self) -> str:
1997
+ """
1998
+ A string identifying the operating system, without a version component, and suitable for presentation to the
1999
+ user. If not set, a default of "NAME=Linux" may be used.
2000
+
2001
+ Examples: "NAME=Fedora", "NAME="Debian GNU/Linux"".
2002
+ """
2003
+
2004
+ return self.raw['NAME']
2005
+
2006
+ @property
2007
+ def id(self) -> str:
2008
+ """
2009
+ A lower-case string (no spaces or other characters outside of 0-9, a-z, ".", "_" and "-") identifying the
2010
+ operating system, excluding any version information and suitable for processing by scripts or usage in generated
2011
+ filenames. If not set, a default of "ID=linux" may be used. Note that even though this string may not include
2012
+ characters that require shell quoting, quoting may nevertheless be used.
2013
+
2014
+ Examples: "ID=fedora", "ID=debian".
2015
+ """
2016
+
2017
+ return self.raw['ID']
2018
+
2019
+ @property
2020
+ def id_like(self) -> str:
2021
+ """
2022
+ A space-separated list of operating system identifiers in the same syntax as the ID= setting. It should list
2023
+ identifiers of operating systems that are closely related to the local operating system in regards to packaging
2024
+ and programming interfaces, for example listing one or more OS identifiers the local OS is a derivative from. An
2025
+ OS should generally only list other OS identifiers it itself is a derivative of, and not any OSes that are
2026
+ derived from it, though symmetric relationships are possible. Build scripts and similar should check this
2027
+ variable if they need to identify the local operating system and the value of ID= is not recognized. Operating
2028
+ systems should be listed in order of how closely the local operating system relates to the listed ones, starting
2029
+ with the closest. This field is optional.
2030
+
2031
+ Examples: for an operating system with "ID=centos", an assignment of "ID_LIKE="rhel fedora"" would be
2032
+ appropriate. For an operating system with "ID=ubuntu", an assignment of "ID_LIKE=debian" is appropriate.
2033
+ """
2034
+
2035
+ return self.raw['ID_LIKE']
2036
+
2037
+ @property
2038
+ def pretty_name(self) -> str:
2039
+ """
2040
+ A pretty operating system name in a format suitable for presentation to the user. May or may not contain a
2041
+ release code name or OS version of some kind, as suitable. If not set, a default of "PRETTY_NAME="Linux"" may be
2042
+ used
2043
+
2044
+ Example: "PRETTY_NAME="Fedora 17 (Beefy Miracle)"".
2045
+ """
2046
+
2047
+ return self.raw['PRETTY_NAME']
2048
+
2049
+ @property
2050
+ def cpe_name(self) -> str:
2051
+ """
2052
+ A CPE name for the operating system, in URI binding syntax, following the Common Platform Enumeration
2053
+ Specification[4] as proposed by the NIST. This field is optional.
2054
+
2055
+ Example: "CPE_NAME="cpe:/o:fedoraproject:fedora:17""
2056
+ """
2057
+
2058
+ return self.raw['CPE_NAME']
2059
+
2060
+ @property
2061
+ def variant(self) -> str:
2062
+ """
2063
+ A string identifying a specific variant or edition of the operating system suitable for presentation to the
2064
+ user. This field may be used to inform the user that the configuration of this system is subject to a specific
2065
+ divergent set of rules or default configuration settings. This field is optional and may not be implemented on
2066
+ all systems.
2067
+
2068
+ Examples: "VARIANT="Server Edition"", "VARIANT="Smart Refrigerator Edition"".
2069
+
2070
+ Note: this field is for display purposes only. The VARIANT_ID field should be used for making programmatic
2071
+ decisions.
2072
+
2073
+ Added in version 220.
2074
+ """
2075
+
2076
+ return self.raw['VARIANT']
2077
+
2078
+ @property
2079
+ def variant_id(self) -> str:
2080
+ """
2081
+ A lower-case string (no spaces or other characters outside of 0-9, a-z, ".", "_" and "-"), identifying a
2082
+ specific variant or edition of the operating system. This may be interpreted by other packages in order to
2083
+ determine a divergent default configuration. This field is optional and may not be implemented on all systems.
2084
+
2085
+ Examples: "VARIANT_ID=server", "VARIANT_ID=embedded".
2086
+
2087
+ Added in version 220.
2088
+ """
2089
+
2090
+ return self.raw['variant_id']
2091
+
2092
+ # Information about the version of the operating system
2093
+
2094
+ @property
2095
+ def version(self) -> str:
2096
+ """
2097
+ A string identifying the operating system version, excluding any OS name information, possibly including a
2098
+ release code name, and suitable for presentation to the user. This field is optional.
2099
+
2100
+ Examples: "VERSION=17", "VERSION="17 (Beefy Miracle)"".
2101
+ """
2102
+
2103
+ return self.raw['VERSION']
2104
+
2105
+ @property
2106
+ def version_id(self) -> str:
2107
+ """
2108
+ A lower-case string (mostly numeric, no spaces or other characters outside of 0-9, a-z, ".", "_" and "-")
2109
+ identifying the operating system version, excluding any OS name information or release code name, and suitable
2110
+ for processing by scripts or usage in generated filenames. This field is optional.
2111
+
2112
+ Examples: "VERSION_ID=17", "VERSION_ID=11.04".
2113
+ """
2114
+
2115
+ return self.raw['VERSION_ID']
2116
+
2117
+ @property
2118
+ def version_codename(self) -> str:
2119
+ """
2120
+ A lower-case string (no spaces or other characters outside of 0-9, a-z, ".", "_" and "-") identifying the
2121
+ operating system release code name, excluding any OS name information or release version, and suitable for
2122
+ processing by scripts or usage in generated filenames. This field is optional and may not be implemented on all
2123
+ systems.
2124
+
2125
+ Examples: "VERSION_CODENAME=buster", "VERSION_CODENAME=xenial".
2126
+
2127
+ Added in version 231.
2128
+ """
2129
+
2130
+ return self.raw['VERSION_CODENAME']
2131
+
2132
+ @property
2133
+ def build_id(self) -> str:
2134
+ """
2135
+ A string uniquely identifying the system image originally used as the installation base. In most cases,
2136
+ VERSION_ID or IMAGE_ID+IMAGE_VERSION are updated when the entire system image is replaced during an update.
2137
+ BUILD_ID may be used in distributions where the original installation image version is important: VERSION_ID
2138
+ would change during incremental system updates, but BUILD_ID would not. This field is optional.
2139
+
2140
+ Examples: "BUILD_ID="2013-03-20.3"", "BUILD_ID=201303203".
2141
+
2142
+ Added in version 200.
2143
+ """
2144
+
2145
+ return self.raw['BUILD_ID']
2146
+
2147
+ @property
2148
+ def image_id(self) -> str:
2149
+ """
2150
+ A lower-case string (no spaces or other characters outside of 0-9, a-z, ".", "_" and "-"), identifying a
2151
+ specific image of the operating system. This is supposed to be used for environments where OS images are
2152
+ prepared, built, shipped and updated as comprehensive, consistent OS images. This field is optional and may not
2153
+ be implemented on all systems, in particularly not on those that are not managed via images but put together and
2154
+ updated from individual packages and on the local system.
2155
+
2156
+ Examples: "IMAGE_ID=vendorx-cashier-system", "IMAGE_ID=netbook-image".
2157
+
2158
+ Added in version 249.
2159
+ """
2160
+
2161
+ return self.raw['IMAGE_ID']
2162
+
2163
+ @property
2164
+ def image_version(self) -> str:
2165
+ """
2166
+ A lower-case string (mostly numeric, no spaces or other characters outside of 0-9, a-z, ".", "_" and "-")
2167
+ identifying the OS image version. This is supposed to be used together with IMAGE_ID described above, to discern
2168
+ different versions of the same image.
2169
+
2170
+ Examples: "IMAGE_VERSION=33", "IMAGE_VERSION=47.1rc1".
2171
+
2172
+ Added in version 249.
2173
+ """
2174
+
2175
+ return self.raw['IMAGE_VERSION']
2176
+
2177
+ # To summarize: if the image updates are built and shipped as comprehensive units, IMAGE_ID+IMAGE_VERSION is the
2178
+ # best fit. Otherwise, if updates eventually completely replace previously installed contents, as in a typical
2179
+ # binary distribution, VERSION_ID should be used to identify major releases of the operating system. BUILD_ID may
2180
+ # be used instead or in addition to VERSION_ID when the original system image version is important.
2181
+
2182
+ #
2183
+
2184
+ # Presentation information and links
2185
+
2186
+ # Links to resources on the Internet related to the operating system. HOME_URL= should refer to the homepage of the
2187
+ # operating system, or alternatively some homepage of the specific version of the operating system.
2188
+ # DOCUMENTATION_URL= should refer to the main documentation page for this operating system. SUPPORT_URL= should
2189
+ # refer to the main support page for the operating system, if there is any. This is primarily intended for operating
2190
+ # systems which vendors provide support for. BUG_REPORT_URL= should refer to the main bug reporting page for the
2191
+ # operating system, if there is any. This is primarily intended for operating systems that rely on community QA.
2192
+ # PRIVACY_POLICY_URL= should refer to the main privacy policy page for the operating system, if there is any. These
2193
+ # settings are optional, and providing only some of these settings is common. These URLs are intended to be exposed
2194
+ # in "About this system" UIs behind links with captions such as "About this Operating System", "Obtain Support",
2195
+ # "Report a Bug", or "Privacy Policy". The values should be in RFC3986 format[5], and should be "http:" or "https:"
2196
+ # URLs, and possibly "mailto:" or "tel:". Only one URL shall be listed in each setting. If multiple resources need
2197
+ # to be referenced, it is recommended to provide an online landing page linking all available resources.
2198
+
2199
+ # Examples: "HOME_URL="https://fedoraproject.org/"", "BUG_REPORT_URL="https://bugzilla.redhat.com/"".
2200
+
2201
+ @property
2202
+ def home_url(self) -> str:
2203
+ return self.raw['HOME_URL']
2204
+
2205
+ @property
2206
+ def documentation_url(self) -> str:
2207
+ return self.raw['DOCUMENTATION_URL']
2208
+
2209
+ @property
2210
+ def support_url(self) -> str:
2211
+ return self.raw['SUPPORT_URL']
2212
+
2213
+ @property
2214
+ def bug_report_url(self) -> str:
2215
+ return self.raw['BUG_REPORT_URL']
2216
+
2217
+ @property
2218
+ def privacy_policy_url(self) -> str:
2219
+ return self.raw['PRIVACY_POLICY_URL']
2220
+
2221
+ @property
2222
+ def support_end(self) -> str:
2223
+ """
2224
+ The date at which support for this version of the OS ends. (What exactly "lack of support" means varies between
2225
+ vendors, but generally users should assume that updates, including security fixes, will not be provided.) The
2226
+ value is a date in the ISO 8601 format "YYYY-MM-DD", and specifies the first day on which support is not
2227
+ provided.
2228
+
2229
+ For example, "SUPPORT_END=2001-01-01" means that the system was supported until the end of the last day of the
2230
+ previous millennium.
2231
+
2232
+ Added in version 252.
2233
+ """
2234
+
2235
+ return self.raw['SUPPORT_END']
2236
+
2237
+ @property
2238
+ def logo(self) -> str:
2239
+ """
2240
+ A string, specifying the name of an icon as defined by freedesktop.org Icon Theme Specification[6]. This can be
2241
+ used by graphical applications to display an operating system's or distributor's logo. This field is optional
2242
+ and may not necessarily be implemented on all systems.
2243
+
2244
+ Examples: "LOGO=fedora-logo", "LOGO=distributor-logo-opensuse"
2245
+
2246
+ Added in version 240.
2247
+ """
2248
+
2249
+ return self.raw['LOGO']
2250
+
2251
+ @property
2252
+ def ansi_color(self) -> str:
2253
+ """
2254
+ A suggested presentation color when showing the OS name on the console. This should be specified as string
2255
+ suitable for inclusion in the ESC [ m ANSI/ECMA-48 escape code for setting graphical rendition. This field is
2256
+ optional.
2257
+
2258
+ Examples: "ANSI_COLOR="0;31"" for red, "ANSI_COLOR="1;34"" for light blue, or "ANSI_COLOR="0;38;2;60;110;180""
2259
+ for Fedora blue.
2260
+ """
2261
+
2262
+ return self.raw['ANSI_COLOR']
2263
+
2264
+ @property
2265
+ def vendor_name(self) -> str:
2266
+ """
2267
+ The name of the OS vendor. This is the name of the organization or company which produces the OS. This field is
2268
+ optional.
2269
+
2270
+ This name is intended to be exposed in "About this system" UIs or software update UIs when needed to distinguish
2271
+ the OS vendor from the OS itself. It is intended to be human readable.
2272
+
2273
+ Examples: "VENDOR_NAME="Fedora Project"" for Fedora Linux, "VENDOR_NAME="Canonical"" for Ubuntu.
2274
+
2275
+ Added in version 254.
2276
+ """
2277
+
2278
+ return self.raw['VENDOR_NAME']
2279
+
2280
+ @property
2281
+ def vendor_url(self) -> str:
2282
+ """
2283
+ The homepage of the OS vendor. This field is optional. The VENDOR_NAME= field should be set if this one is,
2284
+ although clients must be robust against either field not being set.
2285
+
2286
+ The value should be in RFC3986 format[5], and should be "http:" or "https:" URLs. Only one URL shall be listed
2287
+ in the setting.
2288
+
2289
+ Examples: "VENDOR_URL="https://fedoraproject.org/"", "VENDOR_URL="https://canonical.com/"".
2290
+
2291
+ Added in version 254.
2292
+ """
2293
+
2294
+ return self.raw['VENDOR_URL']
2295
+
2296
+ # Distribution-level defaults and metadata
2297
+
2298
+ @property
2299
+ def default_hostname(self) -> str:
2300
+ """
2301
+ A string specifying the hostname if hostname(5) is not present and no other configuration source specifies the
2302
+ hostname. Must be either a single DNS label (a string composed of 7-bit ASCII lower-case characters and no
2303
+ spaces or dots, limited to the format allowed for DNS domain name labels), or a sequence of such labels
2304
+ separated by single dots that forms a valid DNS FQDN. The hostname must be at most 64 characters, which is a
2305
+ Linux limitation (DNS allows longer names).
2306
+
2307
+ See org.freedesktop.hostname1(5) for a description of how systemd-hostnamed.service(8) determines the fallback
2308
+ hostname.
2309
+
2310
+ Added in version 248.
2311
+ """
2312
+
2313
+ return self.raw['DEFAULT_HOSTNAME']
2314
+
2315
+ @property
2316
+ def architecture(self) -> str:
2317
+ """
2318
+ A string that specifies which CPU architecture the userspace binaries require. The architecture identifiers are
2319
+ the same as for ConditionArchitecture= described in systemd.unit(5). The field is optional and should only be
2320
+ used when just single architecture is supported. It may provide redundant information when used in a GPT
2321
+ partition with a GUID type that already encodes the architecture. If this is not the case, the architecture
2322
+ should be specified in e.g., an extension image, to prevent an incompatible host from loading it.
2323
+
2324
+ Added in version 252.
2325
+ """
2326
+
2327
+ return self.raw['ARCHITECTURE']
2328
+
2329
+ @property
2330
+ def sysext_level(self) -> str:
2331
+ """
2332
+ A lower-case string (mostly numeric, no spaces or other characters outside of 0-9, a-z, ".", "_" and "-")
2333
+ identifying the operating system extensions support level, to indicate which extension images are supported. See
2334
+ /usr/lib/extension-release.d/extension-release.IMAGE, initrd[2] and systemd-sysext(8)) for more information.
2335
+
2336
+ Examples: "SYSEXT_LEVEL=2", "SYSEXT_LEVEL=15.14".
2337
+
2338
+ Added in version 248.
2339
+ """
2340
+
2341
+ return self.raw['SYSEXT_LEVEL']
2342
+
2343
+ @property
2344
+ def confext_level(self) -> str:
2345
+ """
2346
+ Semantically the same as SYSEXT_LEVEL= but for confext images. See
2347
+ /etc/extension-release.d/extension-release.IMAGE for more information.
2348
+
2349
+ Examples: "CONFEXT_LEVEL=2", "CONFEXT_LEVEL=15.14".
2350
+
2351
+ Added in version 254.
2352
+ """
2353
+
2354
+ return self.raw['CONFEXT_LEVEL']
2355
+
2356
+ @property
2357
+ def sysext_scope(self) -> str:
2358
+ """
2359
+ Takes a space-separated list of one or more of the strings "system", "initrd" and "portable". This field is only
2360
+ supported in extension-release.d/ files and indicates what environments the system extension is applicable to:
2361
+ i.e. to regular systems, to initrds, or to portable service images. If unspecified, "SYSEXT_SCOPE=system
2362
+ portable" is implied, i.e. any system extension without this field is applicable to regular systems and to
2363
+ portable service environments, but not to initrd environments.
2364
+
2365
+ Added in version 250.
2366
+ """
2367
+
2368
+ return self.raw['SYSEXT_SCOPE']
2369
+
2370
+ @property
2371
+ def confext_scope(self) -> str:
2372
+ """
2373
+ Semantically the same as SYSEXT_SCOPE= but for confext images.
2374
+
2375
+ Added in version 254.
2376
+ """
2377
+
2378
+ return self.raw['CONFEXT_SCOPE']
2379
+
2380
+ @property
2381
+ def portable_prefixes(self) -> str:
2382
+ """
2383
+ Takes a space-separated list of one or more valid prefix match strings for the Portable Services[3] logic. This
2384
+ field serves two purposes: it is informational, identifying portable service images as such (and thus allowing
2385
+ them to be distinguished from other OS images, such as bootable system images). It is also used when a portable
2386
+ service image is attached: the specified or implied portable service prefix is checked against the list
2387
+ specified here, to enforce restrictions how images may be attached to a system.
2388
+
2389
+ Added in version 250.
2390
+ """
2391
+
2392
+ return self.raw['PORTABLE_PREFIXES']
2393
+
2394
+ #
2395
+
2396
+ DEFAULT_PATHS: ta.ClassVar[ta.Sequence[str]] = [
2397
+ '/etc/os-release',
2398
+ '/usr/lib/os-release',
2399
+ ]
2400
+
2401
+ @classmethod
2402
+ def read(cls, *paths: str) -> ta.Optional['LinuxOsRelease']:
2403
+ for fp in (paths or cls.DEFAULT_PATHS):
2404
+ if not os.path.isfile(fp):
2405
+ continue
2406
+ with open(fp) as f:
2407
+ src = f.read()
2408
+ break
2409
+ else:
2410
+ return None
2411
+
2412
+ raw = cls._parse_os_release(src)
2413
+
2414
+ return cls(raw)
2415
+
2416
+ @classmethod
2417
+ def _parse_os_release(cls, src: str) -> ta.Mapping[str, str]:
2418
+ dct: ta.Dict[str, str] = {}
2419
+
2420
+ for l in src.splitlines():
2421
+ if not (l := l.strip()):
2422
+ continue
2423
+ if l.startswith('#') or '=' not in l:
2424
+ continue
2425
+
2426
+ k, _, v = l.partition('=')
2427
+ if k.startswith('"'):
2428
+ k = k[1:-1]
2429
+ if v.startswith('"'):
2430
+ v = v[1:-1]
2431
+
2432
+ dct[k] = v
2433
+
2434
+ return dct
2435
+
2436
+
1940
2437
  ########################################
1941
2438
  # ../../../omdev/packaging/specifiers.py
1942
2439
  # Copyright (c) Donald Stufft and individual contributors.
@@ -2610,43 +3107,264 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
2610
3107
 
2611
3108
 
2612
3109
  ########################################
2613
- # ../remote/config.py
3110
+ # ../deploy/paths.py
3111
+ """
3112
+ ~deploy
3113
+ deploy.pid (flock)
3114
+ /app
3115
+ /<appspec> - shallow clone
3116
+ /conf
3117
+ /env
3118
+ <appspec>.env
3119
+ /nginx
3120
+ <appspec>.conf
3121
+ /supervisor
3122
+ <appspec>.conf
3123
+ /venv
3124
+ /<appspec>
3125
+
3126
+ ?
3127
+ /logs
3128
+ /wrmsr--omlish--<spec>
3129
+
3130
+ spec = <name>--<rev>--<when>
3131
+
3132
+ ==
3133
+
3134
+ for dn in [
3135
+ 'app',
3136
+ 'conf',
3137
+ 'conf/env',
3138
+ 'conf/nginx',
3139
+ 'conf/supervisor',
3140
+ 'venv',
3141
+ ]:
3142
+
3143
+ ==
2614
3144
 
3145
+ """
2615
3146
 
2616
- @dc.dataclass(frozen=True)
2617
- class RemoteConfig:
2618
- payload_file: ta.Optional[str] = None
2619
3147
 
2620
- set_pgid: bool = True
3148
+ ##
2621
3149
 
2622
- deathsig: ta.Optional[str] = 'KILL'
2623
3150
 
2624
- pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
3151
+ DEPLOY_PATH_SPEC_PLACEHOLDER = '@'
3152
+ DEPLOY_PATH_SPEC_SEPARATORS = '-.'
2625
3153
 
2626
- forward_logging: bool = True
3154
+ DEPLOY_PATH_SPECS: ta.FrozenSet[str] = frozenset([
3155
+ 'app',
3156
+ 'tag', # <rev>-<dt>
3157
+ ])
2627
3158
 
2628
- timebomb_delay_s: ta.Optional[float] = 60 * 60.
2629
3159
 
2630
- heartbeat_interval_s: float = 3.
3160
+ class DeployPathError(Exception):
3161
+ pass
2631
3162
 
2632
- use_in_process_remote_executor: bool = False
2633
3163
 
3164
+ @dc.dataclass(frozen=True)
3165
+ class DeployPathPart(abc.ABC): # noqa
3166
+ @property
3167
+ @abc.abstractmethod
3168
+ def kind(self) -> DeployPathKind:
3169
+ raise NotImplementedError
2634
3170
 
2635
- ########################################
2636
- # ../remote/payload.py
3171
+ @abc.abstractmethod
3172
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3173
+ raise NotImplementedError
2637
3174
 
2638
3175
 
2639
- RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
3176
+ #
2640
3177
 
2641
3178
 
2642
- @cached_nullary
2643
- def _get_self_src() -> str:
2644
- return inspect.getsource(sys.modules[__name__])
3179
+ class DirDeployPathPart(DeployPathPart, abc.ABC):
3180
+ @property
3181
+ def kind(self) -> DeployPathKind:
3182
+ return 'dir'
2645
3183
 
3184
+ @classmethod
3185
+ def parse(cls, s: str) -> 'DirDeployPathPart':
3186
+ if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
3187
+ check.equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
3188
+ return SpecDirDeployPathPart(s[1:])
3189
+ else:
3190
+ return ConstDirDeployPathPart(s)
2646
3191
 
2647
- def _is_src_amalg(src: str) -> bool:
2648
- for l in src.splitlines(): # noqa
2649
- if l.startswith('# @omlish-amalg-output '):
3192
+
3193
+ class FileDeployPathPart(DeployPathPart, abc.ABC):
3194
+ @property
3195
+ def kind(self) -> DeployPathKind:
3196
+ return 'file'
3197
+
3198
+ @classmethod
3199
+ def parse(cls, s: str) -> 'FileDeployPathPart':
3200
+ if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
3201
+ check.equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
3202
+ if not any(c in s for c in DEPLOY_PATH_SPEC_SEPARATORS):
3203
+ return SpecFileDeployPathPart(s[1:], '')
3204
+ else:
3205
+ p = min(f for c in DEPLOY_PATH_SPEC_SEPARATORS if (f := s.find(c)) > 0)
3206
+ return SpecFileDeployPathPart(s[1:p], s[p:])
3207
+ else:
3208
+ return ConstFileDeployPathPart(s)
3209
+
3210
+
3211
+ #
3212
+
3213
+
3214
+ @dc.dataclass(frozen=True)
3215
+ class ConstDeployPathPart(DeployPathPart, abc.ABC):
3216
+ name: str
3217
+
3218
+ def __post_init__(self) -> None:
3219
+ check.non_empty_str(self.name)
3220
+ check.not_in('/', self.name)
3221
+ check.not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.name)
3222
+
3223
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3224
+ return self.name
3225
+
3226
+
3227
+ class ConstDirDeployPathPart(ConstDeployPathPart, DirDeployPathPart):
3228
+ pass
3229
+
3230
+
3231
+ class ConstFileDeployPathPart(ConstDeployPathPart, FileDeployPathPart):
3232
+ pass
3233
+
3234
+
3235
+ #
3236
+
3237
+
3238
+ @dc.dataclass(frozen=True)
3239
+ class SpecDeployPathPart(DeployPathPart, abc.ABC):
3240
+ spec: str # DeployPathSpec
3241
+
3242
+ def __post_init__(self) -> None:
3243
+ check.non_empty_str(self.spec)
3244
+ for c in [*DEPLOY_PATH_SPEC_SEPARATORS, DEPLOY_PATH_SPEC_PLACEHOLDER, '/']:
3245
+ check.not_in(c, self.spec)
3246
+ check.in_(self.spec, DEPLOY_PATH_SPECS)
3247
+
3248
+ def _render_spec(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3249
+ if specs is not None:
3250
+ return specs[self.spec] # type: ignore
3251
+ else:
3252
+ return DEPLOY_PATH_SPEC_PLACEHOLDER + self.spec
3253
+
3254
+
3255
+ @dc.dataclass(frozen=True)
3256
+ class SpecDirDeployPathPart(SpecDeployPathPart, DirDeployPathPart):
3257
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3258
+ return self._render_spec(specs)
3259
+
3260
+
3261
+ @dc.dataclass(frozen=True)
3262
+ class SpecFileDeployPathPart(SpecDeployPathPart, FileDeployPathPart):
3263
+ suffix: str
3264
+
3265
+ def __post_init__(self) -> None:
3266
+ super().__post_init__()
3267
+ if self.suffix:
3268
+ for c in [DEPLOY_PATH_SPEC_PLACEHOLDER, '/']:
3269
+ check.not_in(c, self.suffix)
3270
+
3271
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3272
+ return self._render_spec(specs) + self.suffix
3273
+
3274
+
3275
+ ##
3276
+
3277
+
3278
+ @dc.dataclass(frozen=True)
3279
+ class DeployPath:
3280
+ parts: ta.Sequence[DeployPathPart]
3281
+
3282
+ def __post_init__(self) -> None:
3283
+ check.not_empty(self.parts)
3284
+ for p in self.parts[:-1]:
3285
+ check.equal(p.kind, 'dir')
3286
+
3287
+ pd = {}
3288
+ for i, p in enumerate(self.parts):
3289
+ if isinstance(p, SpecDeployPathPart):
3290
+ if p.spec in pd:
3291
+ raise DeployPathError('Duplicate specs in path', self)
3292
+ pd[p.spec] = i
3293
+
3294
+ if 'tag' in pd:
3295
+ if 'app' not in pd or pd['app'] >= pd['tag']:
3296
+ raise DeployPathError('Tag spec in path without preceding app', self)
3297
+
3298
+ @property
3299
+ def kind(self) -> ta.Literal['file', 'dir']:
3300
+ return self.parts[-1].kind
3301
+
3302
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
3303
+ return os.path.join( # noqa
3304
+ *[p.render(specs) for p in self.parts],
3305
+ *([''] if self.kind == 'dir' else []),
3306
+ )
3307
+
3308
+ @classmethod
3309
+ def parse(cls, s: str) -> 'DeployPath':
3310
+ tail_parse: ta.Callable[[str], DeployPathPart]
3311
+ if s.endswith('/'):
3312
+ tail_parse = DirDeployPathPart.parse
3313
+ s = s[:-1]
3314
+ else:
3315
+ tail_parse = FileDeployPathPart.parse
3316
+ ps = check.non_empty_str(s).split('/')
3317
+ return cls([
3318
+ *([DirDeployPathPart.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
3319
+ tail_parse(ps[-1]),
3320
+ ])
3321
+
3322
+
3323
+ ##
3324
+
3325
+
3326
+ class DeployPathOwner(abc.ABC):
3327
+ @abc.abstractmethod
3328
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
3329
+ raise NotImplementedError
3330
+
3331
+
3332
+ ########################################
3333
+ # ../remote/config.py
3334
+
3335
+
3336
+ @dc.dataclass(frozen=True)
3337
+ class RemoteConfig:
3338
+ payload_file: ta.Optional[str] = None
3339
+
3340
+ set_pgid: bool = True
3341
+
3342
+ deathsig: ta.Optional[str] = 'KILL'
3343
+
3344
+ pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
3345
+
3346
+ forward_logging: bool = True
3347
+
3348
+ timebomb_delay_s: ta.Optional[float] = 60 * 60.
3349
+
3350
+ heartbeat_interval_s: float = 3.
3351
+
3352
+
3353
+ ########################################
3354
+ # ../remote/payload.py
3355
+
3356
+
3357
+ RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
3358
+
3359
+
3360
+ @cached_nullary
3361
+ def _get_self_src() -> str:
3362
+ return inspect.getsource(sys.modules[__name__])
3363
+
3364
+
3365
+ def _is_src_amalg(src: str) -> bool:
3366
+ for l in src.splitlines(): # noqa
3367
+ if l.startswith('# @omlish-amalg-output '):
2650
3368
  return True
2651
3369
  return False
2652
3370
 
@@ -2671,6 +3389,90 @@ def get_remote_payload_src(
2671
3389
  return importlib.resources.files(__package__.split('.')[0] + '.scripts').joinpath('manage.py').read_text()
2672
3390
 
2673
3391
 
3392
+ ########################################
3393
+ # ../targets/targets.py
3394
+ """
3395
+ It's desugaring. Subprocess and locals are only leafs. Retain an origin?
3396
+ ** TWO LAYERS ** - ManageTarget is user-facing, ConnectorTarget is bound, internal
3397
+ """
3398
+
3399
+
3400
+ ##
3401
+
3402
+
3403
+ class ManageTarget(abc.ABC): # noqa
3404
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
3405
+ super().__init_subclass__(**kwargs)
3406
+
3407
+ check.state(cls.__name__.endswith('ManageTarget'))
3408
+
3409
+
3410
+ #
3411
+
3412
+
3413
+ @dc.dataclass(frozen=True)
3414
+ class PythonRemoteManageTarget:
3415
+ DEFAULT_PYTHON: ta.ClassVar[str] = 'python3'
3416
+ python: str = DEFAULT_PYTHON
3417
+
3418
+
3419
+ #
3420
+
3421
+
3422
+ class RemoteManageTarget(ManageTarget, abc.ABC):
3423
+ pass
3424
+
3425
+
3426
+ class PhysicallyRemoteManageTarget(RemoteManageTarget, abc.ABC):
3427
+ pass
3428
+
3429
+
3430
+ class LocalManageTarget(ManageTarget, abc.ABC):
3431
+ pass
3432
+
3433
+
3434
+ ##
3435
+
3436
+
3437
+ @dc.dataclass(frozen=True)
3438
+ class SshManageTarget(PhysicallyRemoteManageTarget, PythonRemoteManageTarget):
3439
+ host: ta.Optional[str] = None
3440
+ username: ta.Optional[str] = None
3441
+ key_file: ta.Optional[str] = None
3442
+
3443
+ def __post_init__(self) -> None:
3444
+ check.non_empty_str(self.host)
3445
+
3446
+
3447
+ ##
3448
+
3449
+
3450
+ @dc.dataclass(frozen=True)
3451
+ class DockerManageTarget(RemoteManageTarget, PythonRemoteManageTarget): # noqa
3452
+ image: ta.Optional[str] = None
3453
+ container_id: ta.Optional[str] = None
3454
+
3455
+ def __post_init__(self) -> None:
3456
+ check.arg(bool(self.image) ^ bool(self.container_id))
3457
+
3458
+
3459
+ ##
3460
+
3461
+
3462
+ @dc.dataclass(frozen=True)
3463
+ class InProcessManageTarget(LocalManageTarget):
3464
+ class Mode(enum.Enum):
3465
+ DIRECT = enum.auto()
3466
+ FAKE_REMOTE = enum.auto()
3467
+
3468
+ mode: Mode = Mode.DIRECT
3469
+
3470
+
3471
+ @dc.dataclass(frozen=True)
3472
+ class SubprocessManageTarget(LocalManageTarget, PythonRemoteManageTarget):
3473
+ pass
3474
+
3475
+
2674
3476
  ########################################
2675
3477
  # ../../../omlish/argparse/cli.py
2676
3478
  """
@@ -2943,6 +3745,78 @@ class ArgparseCli:
2943
3745
  return await fn()
2944
3746
 
2945
3747
 
3748
+ ########################################
3749
+ # ../../../omlish/lite/contextmanagers.py
3750
+
3751
+
3752
+ ##
3753
+
3754
+
3755
+ class ExitStacked:
3756
+ _exit_stack: ta.Optional[contextlib.ExitStack] = None
3757
+
3758
+ def __enter__(self: ExitStackedT) -> ExitStackedT:
3759
+ check.state(self._exit_stack is None)
3760
+ es = self._exit_stack = contextlib.ExitStack()
3761
+ es.__enter__()
3762
+ return self
3763
+
3764
+ def __exit__(self, exc_type, exc_val, exc_tb):
3765
+ if (es := self._exit_stack) is None:
3766
+ return None
3767
+ self._exit_contexts()
3768
+ return es.__exit__(exc_type, exc_val, exc_tb)
3769
+
3770
+ def _exit_contexts(self) -> None:
3771
+ pass
3772
+
3773
+ def _enter_context(self, cm: ta.ContextManager[T]) -> T:
3774
+ es = check.not_none(self._exit_stack)
3775
+ return es.enter_context(cm)
3776
+
3777
+
3778
+ ##
3779
+
3780
+
3781
+ @contextlib.contextmanager
3782
+ def defer(fn: ta.Callable) -> ta.Generator[ta.Callable, None, None]:
3783
+ try:
3784
+ yield fn
3785
+ finally:
3786
+ fn()
3787
+
3788
+
3789
+ @contextlib.contextmanager
3790
+ def attr_setting(obj, attr, val, *, default=None): # noqa
3791
+ not_set = object()
3792
+ orig = getattr(obj, attr, not_set)
3793
+ try:
3794
+ setattr(obj, attr, val)
3795
+ if orig is not not_set:
3796
+ yield orig
3797
+ else:
3798
+ yield default
3799
+ finally:
3800
+ if orig is not_set:
3801
+ delattr(obj, attr)
3802
+ else:
3803
+ setattr(obj, attr, orig)
3804
+
3805
+
3806
+ ##
3807
+
3808
+
3809
+ class aclosing(contextlib.AbstractAsyncContextManager): # noqa
3810
+ def __init__(self, thing):
3811
+ self.thing = thing
3812
+
3813
+ async def __aenter__(self):
3814
+ return self.thing
3815
+
3816
+ async def __aexit__(self, *exc_info):
3817
+ await self.thing.aclose()
3818
+
3819
+
2946
3820
  ########################################
2947
3821
  # ../../../omlish/lite/inject.py
2948
3822
 
@@ -4111,7 +4985,8 @@ def configure_standard_logging(
4111
4985
  """
4112
4986
  TODO:
4113
4987
  - pickle stdlib objs? have to pin to 3.8 pickle protocol, will be cross-version
4114
- - nonstrict toggle
4988
+ - namedtuple
4989
+ - literals
4115
4990
  """
4116
4991
 
4117
4992
 
@@ -4401,14 +5276,18 @@ class ObjMarshalerManager:
4401
5276
  ) -> ObjMarshaler:
4402
5277
  if isinstance(ty, type):
4403
5278
  if abc.ABC in ty.__bases__:
4404
- return PolymorphicObjMarshaler.of([ # type: ignore
5279
+ impls = [ity for ity in deep_subclasses(ty) if abc.ABC not in ity.__bases__] # type: ignore
5280
+ if all(ity.__qualname__.endswith(ty.__name__) for ity in impls):
5281
+ ins = {ity: snake_case(ity.__qualname__[:-len(ty.__name__)]) for ity in impls}
5282
+ else:
5283
+ ins = {ity: ity.__qualname__ for ity in impls}
5284
+ return PolymorphicObjMarshaler.of([
4405
5285
  PolymorphicObjMarshaler.Impl(
4406
5286
  ity,
4407
- ity.__qualname__,
5287
+ itn,
4408
5288
  rec(ity),
4409
5289
  )
4410
- for ity in deep_subclasses(ty)
4411
- if abc.ABC not in ity.__bases__
5290
+ for ity, itn in ins.items()
4412
5291
  ])
4413
5292
 
4414
5293
  if issubclass(ty, enum.Enum):
@@ -4655,41 +5534,6 @@ class Interp:
4655
5534
  version: InterpVersion
4656
5535
 
4657
5536
 
4658
- ########################################
4659
- # ../bootstrap.py
4660
-
4661
-
4662
- @dc.dataclass(frozen=True)
4663
- class MainBootstrap:
4664
- main_config: MainConfig = MainConfig()
4665
-
4666
- remote_config: RemoteConfig = RemoteConfig()
4667
-
4668
- system_config: SystemConfig = SystemConfig()
4669
-
4670
-
4671
- ########################################
4672
- # ../commands/execution.py
4673
-
4674
-
4675
- CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
4676
-
4677
-
4678
- class LocalCommandExecutor(CommandExecutor):
4679
- def __init__(
4680
- self,
4681
- *,
4682
- command_executors: CommandExecutorMap,
4683
- ) -> None:
4684
- super().__init__()
4685
-
4686
- self._command_executors = command_executors
4687
-
4688
- async def execute(self, cmd: Command) -> Command.Output:
4689
- ce: CommandExecutor = self._command_executors[type(cmd)]
4690
- return await ce.execute(cmd)
4691
-
4692
-
4693
5537
  ########################################
4694
5538
  # ../commands/marshal.py
4695
5539
 
@@ -4715,6 +5559,34 @@ def install_command_marshaling(
4715
5559
  )
4716
5560
 
4717
5561
 
5562
+ ########################################
5563
+ # ../commands/ping.py
5564
+
5565
+
5566
+ ##
5567
+
5568
+
5569
+ @dc.dataclass(frozen=True)
5570
+ class PingCommand(Command['PingCommand.Output']):
5571
+ time: float = dc.field(default_factory=time.time)
5572
+
5573
+ @dc.dataclass(frozen=True)
5574
+ class Output(Command.Output):
5575
+ time: float
5576
+
5577
+
5578
+ class PingCommandExecutor(CommandExecutor[PingCommand, PingCommand.Output]):
5579
+ async def execute(self, cmd: PingCommand) -> PingCommand.Output:
5580
+ return PingCommand.Output(cmd.time)
5581
+
5582
+
5583
+ ########################################
5584
+ # ../commands/types.py
5585
+
5586
+
5587
+ CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
5588
+
5589
+
4718
5590
  ########################################
4719
5591
  # ../deploy/commands.py
4720
5592
 
@@ -4828,24 +5700,72 @@ class RemoteChannelImpl(RemoteChannel):
4828
5700
 
4829
5701
 
4830
5702
  ########################################
4831
- # ../system/commands.py
5703
+ # ../system/platforms.py
4832
5704
 
4833
5705
 
4834
5706
  ##
4835
5707
 
4836
5708
 
4837
5709
  @dc.dataclass(frozen=True)
4838
- class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
4839
- @dc.dataclass(frozen=True)
4840
- class Output(Command.Output):
4841
- pass
5710
+ class Platform(abc.ABC): # noqa
5711
+ pass
4842
5712
 
4843
5713
 
4844
- class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
4845
- async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
4846
- log.info('Checking system package!')
5714
+ class LinuxPlatform(Platform, abc.ABC):
5715
+ pass
5716
+
5717
+
5718
+ class UbuntuPlatform(LinuxPlatform):
5719
+ pass
5720
+
5721
+
5722
+ class AmazonLinuxPlatform(LinuxPlatform):
5723
+ pass
5724
+
5725
+
5726
+ class GenericLinuxPlatform(LinuxPlatform):
5727
+ pass
5728
+
5729
+
5730
+ class DarwinPlatform(Platform):
5731
+ pass
5732
+
5733
+
5734
+ class UnknownPlatform(Platform):
5735
+ pass
5736
+
5737
+
5738
+ ##
5739
+
5740
+
5741
+ def _detect_system_platform() -> Platform:
5742
+ plat = sys.platform
5743
+
5744
+ if plat == 'linux':
5745
+ if (osr := LinuxOsRelease.read()) is None:
5746
+ return GenericLinuxPlatform()
5747
+
5748
+ if osr.id == 'amzn':
5749
+ return AmazonLinuxPlatform()
5750
+
5751
+ elif osr.id == 'ubuntu':
5752
+ return UbuntuPlatform()
5753
+
5754
+ else:
5755
+ return GenericLinuxPlatform()
4847
5756
 
4848
- return CheckSystemPackageCommand.Output()
5757
+ elif plat == 'darwin':
5758
+ return DarwinPlatform()
5759
+
5760
+ else:
5761
+ return UnknownPlatform()
5762
+
5763
+
5764
+ @cached_nullary
5765
+ def detect_system_platform() -> Platform:
5766
+ platform = _detect_system_platform()
5767
+ log.info('Detected platform: %r', platform)
5768
+ return platform
4849
5769
 
4850
5770
 
4851
5771
  ########################################
@@ -5032,6 +5952,25 @@ def subprocess_close(
5032
5952
  proc.wait(timeout)
5033
5953
 
5034
5954
 
5955
+ ########################################
5956
+ # ../commands/local.py
5957
+
5958
+
5959
+ class LocalCommandExecutor(CommandExecutor):
5960
+ def __init__(
5961
+ self,
5962
+ *,
5963
+ command_executors: CommandExecutorMap,
5964
+ ) -> None:
5965
+ super().__init__()
5966
+
5967
+ self._command_executors = command_executors
5968
+
5969
+ async def execute(self, cmd: Command) -> Command.Output:
5970
+ ce: CommandExecutor = self._command_executors[type(cmd)]
5971
+ return await ce.execute(cmd)
5972
+
5973
+
5035
5974
  ########################################
5036
5975
  # ../remote/execution.py
5037
5976
  """
@@ -5409,7 +6348,7 @@ class RemoteCommandExecutor(CommandExecutor):
5409
6348
  self,
5410
6349
  cmd: Command,
5411
6350
  *,
5412
- log: ta.Optional[logging.Logger] = None,
6351
+ log: ta.Optional[logging.Logger] = None, # noqa
5413
6352
  omit_exc_object: bool = False,
5414
6353
  ) -> CommandOutputOrException:
5415
6354
  try:
@@ -5429,6 +6368,15 @@ class RemoteCommandExecutor(CommandExecutor):
5429
6368
  return r
5430
6369
 
5431
6370
 
6371
+ ########################################
6372
+ # ../system/config.py
6373
+
6374
+
6375
+ @dc.dataclass(frozen=True)
6376
+ class SystemConfig:
6377
+ platform: ta.Optional[Platform] = None
6378
+
6379
+
5432
6380
  ########################################
5433
6381
  # ../../../omlish/lite/asyncio/subprocesses.py
5434
6382
 
@@ -5591,6 +6539,13 @@ async def asyncio_subprocess_communicate(
5591
6539
  return await AsyncioProcessCommunicator(proc).communicate(input, timeout) # noqa
5592
6540
 
5593
6541
 
6542
+ @dc.dataclass(frozen=True)
6543
+ class AsyncioSubprocessOutput:
6544
+ proc: asyncio.subprocess.Process
6545
+ stdout: ta.Optional[bytes]
6546
+ stderr: ta.Optional[bytes]
6547
+
6548
+
5594
6549
  async def asyncio_subprocess_run(
5595
6550
  *args: str,
5596
6551
  input: ta.Any = None, # noqa
@@ -5598,7 +6553,7 @@ async def asyncio_subprocess_run(
5598
6553
  check: bool = False, # noqa
5599
6554
  capture_output: ta.Optional[bool] = None,
5600
6555
  **kwargs: ta.Any,
5601
- ) -> ta.Tuple[ta.Optional[bytes], ta.Optional[bytes]]:
6556
+ ) -> AsyncioSubprocessOutput:
5602
6557
  if capture_output:
5603
6558
  kwargs.setdefault('stdout', subprocess.PIPE)
5604
6559
  kwargs.setdefault('stderr', subprocess.PIPE)
@@ -5617,7 +6572,11 @@ async def asyncio_subprocess_run(
5617
6572
  stderr=stderr,
5618
6573
  )
5619
6574
 
5620
- return stdout, stderr
6575
+ return AsyncioSubprocessOutput(
6576
+ proc,
6577
+ stdout,
6578
+ stderr,
6579
+ )
5621
6580
 
5622
6581
 
5623
6582
  ##
@@ -5630,7 +6589,7 @@ async def asyncio_subprocess_check_call(
5630
6589
  timeout: ta.Optional[float] = None,
5631
6590
  **kwargs: ta.Any,
5632
6591
  ) -> None:
5633
- _, _ = await asyncio_subprocess_run(
6592
+ await asyncio_subprocess_run(
5634
6593
  *args,
5635
6594
  stdout=stdout,
5636
6595
  input=input,
@@ -5646,7 +6605,7 @@ async def asyncio_subprocess_check_output(
5646
6605
  timeout: ta.Optional[float] = None,
5647
6606
  **kwargs: ta.Any,
5648
6607
  ) -> bytes:
5649
- stdout, stderr = await asyncio_subprocess_run(
6608
+ out = await asyncio_subprocess_run(
5650
6609
  *args,
5651
6610
  stdout=asyncio.subprocess.PIPE,
5652
6611
  input=input,
@@ -5655,7 +6614,7 @@ async def asyncio_subprocess_check_output(
5655
6614
  **kwargs,
5656
6615
  )
5657
6616
 
5658
- return check.not_none(stdout)
6617
+ return check.not_none(out.stdout)
5659
6618
 
5660
6619
 
5661
6620
  async def asyncio_subprocess_check_output_str(*args: str, **kwargs: ta.Any) -> str:
@@ -5811,6 +6770,21 @@ class InterpInspector:
5811
6770
  INTERP_INSPECTOR = InterpInspector()
5812
6771
 
5813
6772
 
6773
+ ########################################
6774
+ # ../bootstrap.py
6775
+
6776
+
6777
+ @dc.dataclass(frozen=True)
6778
+ class MainBootstrap:
6779
+ main_config: MainConfig = MainConfig()
6780
+
6781
+ deploy_config: DeployConfig = DeployConfig()
6782
+
6783
+ remote_config: RemoteConfig = RemoteConfig()
6784
+
6785
+ system_config: SystemConfig = SystemConfig()
6786
+
6787
+
5814
6788
  ########################################
5815
6789
  # ../commands/subprocess.py
5816
6790
 
@@ -5882,146 +6856,190 @@ class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCom
5882
6856
 
5883
6857
 
5884
6858
  ########################################
5885
- # ../remote/_main.py
6859
+ # ../deploy/git.py
6860
+ """
6861
+ TODO:
6862
+ - 'repos'?
6863
+
6864
+ git/github.com/wrmsr/omlish <- bootstrap repo
6865
+ - shallow clone off bootstrap into /apps
6866
+
6867
+ github.com/wrmsr/omlish@rev
6868
+ """
5886
6869
 
5887
6870
 
5888
6871
  ##
5889
6872
 
5890
6873
 
5891
- class _RemoteExecutionLogHandler(logging.Handler):
5892
- def __init__(self, fn: ta.Callable[[str], None]) -> None:
5893
- super().__init__()
5894
- self._fn = fn
6874
+ @dc.dataclass(frozen=True)
6875
+ class DeployGitRepo:
6876
+ host: ta.Optional[str] = None
6877
+ username: ta.Optional[str] = None
6878
+ path: ta.Optional[str] = None
5895
6879
 
5896
- def emit(self, record):
5897
- msg = self.format(record)
5898
- self._fn(msg)
6880
+ def __post_init__(self) -> None:
6881
+ check.not_in('..', check.non_empty_str(self.host))
6882
+ check.not_in('.', check.non_empty_str(self.path))
6883
+
6884
+
6885
+ @dc.dataclass(frozen=True)
6886
+ class DeployGitSpec:
6887
+ repo: DeployGitRepo
6888
+ rev: DeployRev
5899
6889
 
5900
6890
 
5901
6891
  ##
5902
6892
 
5903
6893
 
5904
- class _RemoteExecutionMain:
6894
+ class DeployGitManager(DeployPathOwner):
5905
6895
  def __init__(
5906
6896
  self,
5907
- chan: RemoteChannel,
6897
+ *,
6898
+ deploy_home: DeployHome,
5908
6899
  ) -> None:
5909
6900
  super().__init__()
5910
6901
 
5911
- self._chan = chan
5912
-
5913
- self.__bootstrap: ta.Optional[MainBootstrap] = None
5914
- self.__injector: ta.Optional[Injector] = None
5915
-
5916
- @property
5917
- def _bootstrap(self) -> MainBootstrap:
5918
- return check.not_none(self.__bootstrap)
5919
-
5920
- @property
5921
- def _injector(self) -> Injector:
5922
- return check.not_none(self.__injector)
6902
+ self._deploy_home = deploy_home
6903
+ self._dir = os.path.join(deploy_home, 'git')
5923
6904
 
5924
- #
6905
+ self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
5925
6906
 
5926
- def _timebomb_main(
5927
- self,
5928
- delay_s: float,
5929
- *,
5930
- sig: int = signal.SIGINT,
5931
- code: int = 1,
5932
- ) -> None:
5933
- time.sleep(delay_s)
6907
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
6908
+ return {
6909
+ DeployPath.parse('git'),
6910
+ }
5934
6911
 
5935
- if (pgid := os.getpgid(0)) == os.getpid():
5936
- os.killpg(pgid, sig)
6912
+ class RepoDir:
6913
+ def __init__(
6914
+ self,
6915
+ git: 'DeployGitManager',
6916
+ repo: DeployGitRepo,
6917
+ ) -> None:
6918
+ super().__init__()
5937
6919
 
5938
- os._exit(code) # noqa
6920
+ self._git = git
6921
+ self._repo = repo
6922
+ self._dir = os.path.join(
6923
+ self._git._dir, # noqa
6924
+ check.non_empty_str(repo.host),
6925
+ check.non_empty_str(repo.path),
6926
+ )
5939
6927
 
5940
- @cached_nullary
5941
- def _timebomb_thread(self) -> ta.Optional[threading.Thread]:
5942
- if (tbd := self._bootstrap.remote_config.timebomb_delay_s) is None:
5943
- return None
6928
+ @property
6929
+ def repo(self) -> DeployGitRepo:
6930
+ return self._repo
5944
6931
 
5945
- thr = threading.Thread(
5946
- target=functools.partial(self._timebomb_main, tbd),
5947
- name=f'{self.__class__.__name__}.timebomb',
5948
- daemon=True,
5949
- )
6932
+ @property
6933
+ def url(self) -> str:
6934
+ if self._repo.username is not None:
6935
+ return f'{self._repo.username}@{self._repo.host}:{self._repo.path}'
6936
+ else:
6937
+ return f'https://{self._repo.host}/{self._repo.path}'
5950
6938
 
5951
- thr.start()
6939
+ async def _call(self, *cmd: str) -> None:
6940
+ await asyncio_subprocess_check_call(
6941
+ *cmd,
6942
+ cwd=self._dir,
6943
+ )
5952
6944
 
5953
- log.debug('Started timebomb thread: %r', thr)
6945
+ @async_cached_nullary
6946
+ async def init(self) -> None:
6947
+ os.makedirs(self._dir, exist_ok=True)
6948
+ if os.path.exists(os.path.join(self._dir, '.git')):
6949
+ return
5954
6950
 
5955
- return thr
6951
+ await self._call('git', 'init')
6952
+ await self._call('git', 'remote', 'add', 'origin', self.url)
5956
6953
 
5957
- #
6954
+ async def fetch(self, rev: DeployRev) -> None:
6955
+ await self.init()
6956
+ await self._call('git', 'fetch', '--depth=1', 'origin', rev)
5958
6957
 
5959
- @cached_nullary
5960
- def _log_handler(self) -> _RemoteLogHandler:
5961
- return _RemoteLogHandler(self._chan)
6958
+ async def checkout(self, rev: DeployRev, dst_dir: str) -> None:
6959
+ check.state(not os.path.exists(dst_dir))
5962
6960
 
5963
- #
6961
+ await self.fetch(rev)
5964
6962
 
5965
- async def _setup(self) -> None:
5966
- check.none(self.__bootstrap)
5967
- check.none(self.__injector)
6963
+ # FIXME: temp dir swap
6964
+ os.makedirs(dst_dir)
5968
6965
 
5969
- # Bootstrap
6966
+ dst_call = functools.partial(asyncio_subprocess_check_call, cwd=dst_dir)
6967
+ await dst_call('git', 'init')
5970
6968
 
5971
- self.__bootstrap = check.not_none(await self._chan.recv_obj(MainBootstrap))
6969
+ await dst_call('git', 'remote', 'add', 'local', self._dir)
6970
+ await dst_call('git', 'fetch', '--depth=1', 'local', rev)
6971
+ await dst_call('git', 'checkout', rev)
5972
6972
 
5973
- if (prd := self._bootstrap.remote_config.pycharm_remote_debug) is not None:
5974
- pycharm_debug_connect(prd)
6973
+ def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
6974
+ try:
6975
+ return self._repo_dirs[repo]
6976
+ except KeyError:
6977
+ repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
6978
+ return repo_dir
5975
6979
 
5976
- self.__injector = main_bootstrap(self._bootstrap)
6980
+ async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
6981
+ await self.get_repo_dir(spec.repo).checkout(spec.rev, dst_dir)
5977
6982
 
5978
- self._chan.set_marshaler(self._injector[ObjMarshalerManager])
5979
6983
 
5980
- # Post-bootstrap
6984
+ ########################################
6985
+ # ../deploy/venvs.py
6986
+ """
6987
+ TODO:
6988
+ - interp
6989
+ - share more code with pyproject?
6990
+ """
5981
6991
 
5982
- if self._bootstrap.remote_config.set_pgid:
5983
- if os.getpgid(0) != os.getpid():
5984
- log.debug('Setting pgid')
5985
- os.setpgid(0, 0)
5986
6992
 
5987
- if (ds := self._bootstrap.remote_config.deathsig) is not None:
5988
- log.debug('Setting deathsig: %s', ds)
5989
- set_process_deathsig(int(signal.Signals[f'SIG{ds.upper()}']))
6993
+ class DeployVenvManager(DeployPathOwner):
6994
+ def __init__(
6995
+ self,
6996
+ *,
6997
+ deploy_home: DeployHome,
6998
+ ) -> None:
6999
+ super().__init__()
5990
7000
 
5991
- self._timebomb_thread()
7001
+ self._deploy_home = deploy_home
7002
+ self._dir = os.path.join(deploy_home, 'venvs')
5992
7003
 
5993
- if self._bootstrap.remote_config.forward_logging:
5994
- log.debug('Installing log forwarder')
5995
- logging.root.addHandler(self._log_handler())
7004
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
7005
+ return {
7006
+ DeployPath.parse('venvs/@app/@tag/'),
7007
+ }
5996
7008
 
5997
- #
7009
+ async def setup_venv(
7010
+ self,
7011
+ app_dir: str,
7012
+ venv_dir: str,
7013
+ *,
7014
+ use_uv: bool = True,
7015
+ ) -> None:
7016
+ sys_exe = 'python3'
5998
7017
 
5999
- async def run(self) -> None:
6000
- await self._setup()
7018
+ await asyncio_subprocess_check_call(sys_exe, '-m', 'venv', venv_dir)
6001
7019
 
6002
- executor = self._injector[LocalCommandExecutor]
7020
+ #
6003
7021
 
6004
- handler = _RemoteCommandHandler(self._chan, executor)
7022
+ venv_exe = os.path.join(venv_dir, 'bin', 'python3')
6005
7023
 
6006
- await handler.run()
7024
+ #
6007
7025
 
7026
+ reqs_txt = os.path.join(app_dir, 'requirements.txt')
6008
7027
 
6009
- def _remote_execution_main() -> None:
6010
- rt = pyremote_bootstrap_finalize() # noqa
7028
+ if os.path.isfile(reqs_txt):
7029
+ if use_uv:
7030
+ await asyncio_subprocess_check_call(venv_exe, '-m', 'pip', 'install', 'uv')
7031
+ pip_cmd = ['-m', 'uv', 'pip']
7032
+ else:
7033
+ pip_cmd = ['-m', 'pip']
6011
7034
 
6012
- async def inner() -> None:
6013
- input = await asyncio_open_stream_reader(rt.input) # noqa
6014
- output = await asyncio_open_stream_writer(rt.output)
7035
+ await asyncio_subprocess_check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
6015
7036
 
6016
- chan = RemoteChannelImpl(
6017
- input,
6018
- output,
7037
+ async def setup_app_venv(self, app_tag: DeployAppTag) -> None:
7038
+ await self.setup_venv(
7039
+ os.path.join(self._deploy_home, 'apps', app_tag.app, app_tag.tag),
7040
+ os.path.join(self._deploy_home, 'venvs', app_tag.app, app_tag.tag),
6019
7041
  )
6020
7042
 
6021
- await _RemoteExecutionMain(chan).run()
6022
-
6023
- asyncio.run(inner())
6024
-
6025
7043
 
6026
7044
  ########################################
6027
7045
  # ../remote/spawning.py
@@ -6196,25 +7214,24 @@ class AptSystemPackageManager(SystemPackageManager):
6196
7214
  }
6197
7215
 
6198
7216
  async def update(self) -> None:
6199
- await asyncio_subprocess_check_call('apt', 'update', env={**os.environ, **self._APT_ENV})
7217
+ await asyncio_subprocess_check_call('sudo', 'apt', 'update', env={**os.environ, **self._APT_ENV})
6200
7218
 
6201
7219
  async def upgrade(self) -> None:
6202
- await asyncio_subprocess_check_call('apt', 'upgrade', '-y', env={**os.environ, **self._APT_ENV})
7220
+ await asyncio_subprocess_check_call('sudo', 'apt', 'upgrade', '-y', env={**os.environ, **self._APT_ENV})
6203
7221
 
6204
7222
  async def install(self, *packages: SystemPackageOrStr) -> None:
6205
7223
  pns = [p.name if isinstance(p, SystemPackage) else p for p in packages] # FIXME: versions
6206
- await asyncio_subprocess_check_call('apt', 'install', '-y', *pns, env={**os.environ, **self._APT_ENV})
7224
+ await asyncio_subprocess_check_call('sudo', 'apt', 'install', '-y', *pns, env={**os.environ, **self._APT_ENV})
6207
7225
 
6208
7226
  async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
6209
7227
  pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
6210
- cmd = ['dpkg-query', '-W', '-f=${Package}=${Version}\n', *pns]
6211
- stdout, stderr = await asyncio_subprocess_run(
6212
- *cmd,
7228
+ out = await asyncio_subprocess_run(
7229
+ 'dpkg-query', '-W', '-f=${Package}=${Version}\n', *pns,
6213
7230
  capture_output=True,
6214
7231
  check=False,
6215
7232
  )
6216
7233
  d: ta.Dict[str, SystemPackage] = {}
6217
- for l in check.not_none(stdout).decode('utf-8').strip().splitlines():
7234
+ for l in check.not_none(out.stdout).decode('utf-8').strip().splitlines():
6218
7235
  n, v = l.split('=', 1)
6219
7236
  d[n] = SystemPackage(
6220
7237
  name=n,
@@ -6223,6 +7240,33 @@ class AptSystemPackageManager(SystemPackageManager):
6223
7240
  return d
6224
7241
 
6225
7242
 
7243
+ class YumSystemPackageManager(SystemPackageManager):
7244
+ async def update(self) -> None:
7245
+ await asyncio_subprocess_check_call('sudo', 'yum', 'check-update')
7246
+
7247
+ async def upgrade(self) -> None:
7248
+ await asyncio_subprocess_check_call('sudo', 'yum', 'update')
7249
+
7250
+ async def install(self, *packages: SystemPackageOrStr) -> None:
7251
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages] # FIXME: versions
7252
+ await asyncio_subprocess_check_call('sudo', 'yum', 'install', *pns)
7253
+
7254
+ async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
7255
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
7256
+ d: ta.Dict[str, SystemPackage] = {}
7257
+ for pn in pns:
7258
+ out = await asyncio_subprocess_run(
7259
+ 'rpm', '-q', pn,
7260
+ capture_output=True,
7261
+ )
7262
+ if not out.proc.returncode:
7263
+ d[pn] = SystemPackage(
7264
+ pn,
7265
+ check.not_none(out.stdout).decode().strip(),
7266
+ )
7267
+ return d
7268
+
7269
+
6226
7270
  ########################################
6227
7271
  # ../../../omdev/interp/providers.py
6228
7272
  """
@@ -6285,139 +7329,342 @@ class RunningInterpProvider(InterpProvider):
6285
7329
 
6286
7330
 
6287
7331
  ########################################
6288
- # ../remote/connection.py
7332
+ # ../commands/inject.py
6289
7333
 
6290
7334
 
6291
7335
  ##
6292
7336
 
6293
7337
 
6294
- class RemoteExecutionConnector(abc.ABC):
6295
- @abc.abstractmethod
6296
- def connect(
6297
- self,
6298
- tgt: RemoteSpawning.Target,
6299
- bs: MainBootstrap,
6300
- ) -> ta.AsyncContextManager[RemoteCommandExecutor]:
6301
- raise NotImplementedError
7338
+ def bind_command(
7339
+ command_cls: ta.Type[Command],
7340
+ executor_cls: ta.Optional[ta.Type[CommandExecutor]],
7341
+ ) -> InjectorBindings:
7342
+ lst: ta.List[InjectorBindingOrBindings] = [
7343
+ inj.bind(CommandRegistration(command_cls), array=True),
7344
+ ]
7345
+
7346
+ if executor_cls is not None:
7347
+ lst.extend([
7348
+ inj.bind(executor_cls, singleton=True),
7349
+ inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
7350
+ ])
7351
+
7352
+ return inj.as_bindings(*lst)
6302
7353
 
6303
7354
 
6304
7355
  ##
6305
7356
 
6306
7357
 
6307
- class PyremoteRemoteExecutionConnector(RemoteExecutionConnector):
6308
- def __init__(
6309
- self,
6310
- *,
6311
- spawning: RemoteSpawning,
6312
- msh: ObjMarshalerManager,
6313
- payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
6314
- ) -> None:
6315
- super().__init__()
7358
+ @dc.dataclass(frozen=True)
7359
+ class _FactoryCommandExecutor(CommandExecutor):
7360
+ factory: ta.Callable[[], CommandExecutor]
6316
7361
 
6317
- self._spawning = spawning
6318
- self._msh = msh
6319
- self._payload_file = payload_file
7362
+ def execute(self, i: Command) -> ta.Awaitable[Command.Output]:
7363
+ return self.factory().execute(i)
7364
+
7365
+
7366
+ ##
7367
+
7368
+
7369
+ def bind_commands(
7370
+ *,
7371
+ main_config: MainConfig,
7372
+ ) -> InjectorBindings:
7373
+ lst: ta.List[InjectorBindingOrBindings] = [
7374
+ inj.bind_array(CommandRegistration),
7375
+ inj.bind_array_type(CommandRegistration, CommandRegistrations),
7376
+
7377
+ inj.bind_array(CommandExecutorRegistration),
7378
+ inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
7379
+
7380
+ inj.bind(build_command_name_map, singleton=True),
7381
+ ]
6320
7382
 
6321
7383
  #
6322
7384
 
6323
- @cached_nullary
6324
- def _payload_src(self) -> str:
6325
- return get_remote_payload_src(file=self._payload_file)
7385
+ def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
7386
+ return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
6326
7387
 
6327
- @cached_nullary
6328
- def _remote_src(self) -> ta.Sequence[str]:
6329
- return [
6330
- self._payload_src(),
6331
- '_remote_execution_main()',
6332
- ]
7388
+ lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
6333
7389
 
6334
- @cached_nullary
6335
- def _spawn_src(self) -> str:
6336
- return pyremote_build_bootstrap_cmd(__package__ or 'manage')
7390
+ #
7391
+
7392
+ def provide_command_executor_map(
7393
+ injector: Injector,
7394
+ crs: CommandExecutorRegistrations,
7395
+ ) -> CommandExecutorMap:
7396
+ dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
7397
+
7398
+ cr: CommandExecutorRegistration
7399
+ for cr in crs:
7400
+ if cr.command_cls in dct:
7401
+ raise KeyError(cr.command_cls)
7402
+
7403
+ factory = functools.partial(injector.provide, cr.executor_cls)
7404
+ if main_config.debug:
7405
+ ce = factory()
7406
+ else:
7407
+ ce = _FactoryCommandExecutor(factory)
7408
+
7409
+ dct[cr.command_cls] = ce
7410
+
7411
+ return CommandExecutorMap(dct)
7412
+
7413
+ lst.extend([
7414
+ inj.bind(provide_command_executor_map, singleton=True),
7415
+
7416
+ inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
7417
+ ])
6337
7418
 
6338
7419
  #
6339
7420
 
6340
- @contextlib.asynccontextmanager
6341
- async def connect(
7421
+ lst.extend([
7422
+ bind_command(PingCommand, PingCommandExecutor),
7423
+ bind_command(SubprocessCommand, SubprocessCommandExecutor),
7424
+ ])
7425
+
7426
+ #
7427
+
7428
+ return inj.as_bindings(*lst)
7429
+
7430
+
7431
+ ########################################
7432
+ # ../deploy/apps.py
7433
+
7434
+
7435
+ def make_deploy_tag(
7436
+ rev: DeployRev,
7437
+ now: ta.Optional[datetime.datetime] = None,
7438
+ ) -> DeployTag:
7439
+ if now is None:
7440
+ now = datetime.datetime.utcnow() # noqa
7441
+ now_fmt = '%Y%m%dT%H%M%S'
7442
+ now_str = now.strftime(now_fmt)
7443
+ return DeployTag('-'.join([rev, now_str]))
7444
+
7445
+
7446
+ class DeployAppManager(DeployPathOwner):
7447
+ def __init__(
6342
7448
  self,
6343
- tgt: RemoteSpawning.Target,
6344
- bs: MainBootstrap,
6345
- ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
6346
- spawn_src = self._spawn_src()
6347
- remote_src = self._remote_src()
7449
+ *,
7450
+ deploy_home: DeployHome,
7451
+ git: DeployGitManager,
7452
+ venvs: DeployVenvManager,
7453
+ ) -> None:
7454
+ super().__init__()
6348
7455
 
6349
- async with self._spawning.spawn(
6350
- tgt,
6351
- spawn_src,
6352
- debug=bs.main_config.debug,
6353
- ) as proc:
6354
- res = await PyremoteBootstrapDriver( # noqa
6355
- remote_src,
6356
- PyremoteBootstrapOptions(
6357
- debug=bs.main_config.debug,
6358
- ),
6359
- ).async_run(
6360
- proc.stdout,
6361
- proc.stdin,
6362
- )
7456
+ self._deploy_home = deploy_home
7457
+ self._git = git
7458
+ self._venvs = venvs
6363
7459
 
6364
- chan = RemoteChannelImpl(
6365
- proc.stdout,
6366
- proc.stdin,
6367
- msh=self._msh,
6368
- )
7460
+ self._dir = os.path.join(deploy_home, 'apps')
6369
7461
 
6370
- await chan.send_obj(bs)
7462
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
7463
+ return {
7464
+ DeployPath.parse('apps/@app/@tag'),
7465
+ }
6371
7466
 
6372
- rce: RemoteCommandExecutor
6373
- async with contextlib.aclosing(RemoteCommandExecutor(chan)) as rce:
6374
- await rce.start()
7467
+ async def prepare_app(
7468
+ self,
7469
+ app: DeployApp,
7470
+ rev: DeployRev,
7471
+ repo: DeployGitRepo,
7472
+ ):
7473
+ app_tag = DeployAppTag(app, make_deploy_tag(rev))
7474
+ app_dir = os.path.join(self._dir, app, app_tag.tag)
7475
+
7476
+ #
7477
+
7478
+ await self._git.checkout(
7479
+ DeployGitSpec(
7480
+ repo=repo,
7481
+ rev=rev,
7482
+ ),
7483
+ app_dir,
7484
+ )
7485
+
7486
+ #
7487
+
7488
+ await self._venvs.setup_app_venv(app_tag)
6375
7489
 
6376
- yield rce
7490
+
7491
+ ########################################
7492
+ # ../remote/_main.py
7493
+
7494
+
7495
+ ##
7496
+
7497
+
7498
+ class _RemoteExecutionLogHandler(logging.Handler):
7499
+ def __init__(self, fn: ta.Callable[[str], None]) -> None:
7500
+ super().__init__()
7501
+ self._fn = fn
7502
+
7503
+ def emit(self, record):
7504
+ msg = self.format(record)
7505
+ self._fn(msg)
6377
7506
 
6378
7507
 
6379
7508
  ##
6380
7509
 
6381
7510
 
6382
- class InProcessRemoteExecutionConnector(RemoteExecutionConnector):
7511
+ class _RemoteExecutionMain:
6383
7512
  def __init__(
6384
7513
  self,
6385
- *,
6386
- msh: ObjMarshalerManager,
6387
- local_executor: LocalCommandExecutor,
7514
+ chan: RemoteChannel,
6388
7515
  ) -> None:
6389
7516
  super().__init__()
6390
7517
 
6391
- self._msh = msh
6392
- self._local_executor = local_executor
7518
+ self._chan = chan
6393
7519
 
6394
- @contextlib.asynccontextmanager
6395
- async def connect(
7520
+ self.__bootstrap: ta.Optional[MainBootstrap] = None
7521
+ self.__injector: ta.Optional[Injector] = None
7522
+
7523
+ @property
7524
+ def _bootstrap(self) -> MainBootstrap:
7525
+ return check.not_none(self.__bootstrap)
7526
+
7527
+ @property
7528
+ def _injector(self) -> Injector:
7529
+ return check.not_none(self.__injector)
7530
+
7531
+ #
7532
+
7533
+ def _timebomb_main(
6396
7534
  self,
6397
- tgt: RemoteSpawning.Target,
6398
- bs: MainBootstrap,
6399
- ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
6400
- r0, w0 = asyncio_create_bytes_channel()
6401
- r1, w1 = asyncio_create_bytes_channel()
7535
+ delay_s: float,
7536
+ *,
7537
+ sig: int = signal.SIGINT,
7538
+ code: int = 1,
7539
+ ) -> None:
7540
+ time.sleep(delay_s)
6402
7541
 
6403
- remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
6404
- local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
7542
+ if (pgid := os.getpgid(0)) == os.getpid():
7543
+ os.killpg(pgid, sig)
6405
7544
 
6406
- rch = _RemoteCommandHandler(
6407
- remote_chan,
6408
- self._local_executor,
7545
+ os._exit(code) # noqa
7546
+
7547
+ @cached_nullary
7548
+ def _timebomb_thread(self) -> ta.Optional[threading.Thread]:
7549
+ if (tbd := self._bootstrap.remote_config.timebomb_delay_s) is None:
7550
+ return None
7551
+
7552
+ thr = threading.Thread(
7553
+ target=functools.partial(self._timebomb_main, tbd),
7554
+ name=f'{self.__class__.__name__}.timebomb',
7555
+ daemon=True,
6409
7556
  )
6410
- rch_task = asyncio.create_task(rch.run()) # noqa
6411
- try:
6412
- rce: RemoteCommandExecutor
6413
- async with contextlib.aclosing(RemoteCommandExecutor(local_chan)) as rce:
6414
- await rce.start()
6415
7557
 
6416
- yield rce
7558
+ thr.start()
6417
7559
 
6418
- finally:
6419
- rch.stop()
6420
- await rch_task
7560
+ log.debug('Started timebomb thread: %r', thr)
7561
+
7562
+ return thr
7563
+
7564
+ #
7565
+
7566
+ @cached_nullary
7567
+ def _log_handler(self) -> _RemoteLogHandler:
7568
+ return _RemoteLogHandler(self._chan)
7569
+
7570
+ #
7571
+
7572
+ async def _setup(self) -> None:
7573
+ check.none(self.__bootstrap)
7574
+ check.none(self.__injector)
7575
+
7576
+ # Bootstrap
7577
+
7578
+ self.__bootstrap = check.not_none(await self._chan.recv_obj(MainBootstrap))
7579
+
7580
+ if (prd := self._bootstrap.remote_config.pycharm_remote_debug) is not None:
7581
+ pycharm_debug_connect(prd)
7582
+
7583
+ self.__injector = main_bootstrap(self._bootstrap)
7584
+
7585
+ self._chan.set_marshaler(self._injector[ObjMarshalerManager])
7586
+
7587
+ # Post-bootstrap
7588
+
7589
+ if self._bootstrap.remote_config.set_pgid:
7590
+ if os.getpgid(0) != os.getpid():
7591
+ log.debug('Setting pgid')
7592
+ os.setpgid(0, 0)
7593
+
7594
+ if (ds := self._bootstrap.remote_config.deathsig) is not None:
7595
+ log.debug('Setting deathsig: %s', ds)
7596
+ set_process_deathsig(int(signal.Signals[f'SIG{ds.upper()}']))
7597
+
7598
+ self._timebomb_thread()
7599
+
7600
+ if self._bootstrap.remote_config.forward_logging:
7601
+ log.debug('Installing log forwarder')
7602
+ logging.root.addHandler(self._log_handler())
7603
+
7604
+ #
7605
+
7606
+ async def run(self) -> None:
7607
+ await self._setup()
7608
+
7609
+ executor = self._injector[LocalCommandExecutor]
7610
+
7611
+ handler = _RemoteCommandHandler(self._chan, executor)
7612
+
7613
+ await handler.run()
7614
+
7615
+
7616
+ def _remote_execution_main() -> None:
7617
+ rt = pyremote_bootstrap_finalize() # noqa
7618
+
7619
+ async def inner() -> None:
7620
+ input = await asyncio_open_stream_reader(rt.input) # noqa
7621
+ output = await asyncio_open_stream_writer(rt.output)
7622
+
7623
+ chan = RemoteChannelImpl(
7624
+ input,
7625
+ output,
7626
+ )
7627
+
7628
+ await _RemoteExecutionMain(chan).run()
7629
+
7630
+ asyncio.run(inner())
7631
+
7632
+
7633
+ ########################################
7634
+ # ../system/commands.py
7635
+
7636
+
7637
+ ##
7638
+
7639
+
7640
+ @dc.dataclass(frozen=True)
7641
+ class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
7642
+ pkgs: ta.Sequence[str] = ()
7643
+
7644
+ def __post_init__(self) -> None:
7645
+ check.not_isinstance(self.pkgs, str)
7646
+
7647
+ @dc.dataclass(frozen=True)
7648
+ class Output(Command.Output):
7649
+ pkgs: ta.Sequence[SystemPackage]
7650
+
7651
+
7652
+ class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
7653
+ def __init__(
7654
+ self,
7655
+ *,
7656
+ mgr: SystemPackageManager,
7657
+ ) -> None:
7658
+ super().__init__()
7659
+
7660
+ self._mgr = mgr
7661
+
7662
+ async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
7663
+ log.info('Checking system package!')
7664
+
7665
+ ret = await self._mgr.query(*cmd.pkgs)
7666
+
7667
+ return CheckSystemPackageCommand.Output(list(ret.values()))
6421
7668
 
6422
7669
 
6423
7670
  ########################################
@@ -6961,54 +8208,183 @@ class SystemInterpProvider(InterpProvider):
6961
8208
  lst.append((e, ev))
6962
8209
  return lst
6963
8210
 
6964
- #
8211
+ #
8212
+
8213
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
8214
+ return [ev for e, ev in await self.exe_versions()]
8215
+
8216
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
8217
+ for e, ev in await self.exe_versions():
8218
+ if ev != version:
8219
+ continue
8220
+ return Interp(
8221
+ exe=e,
8222
+ version=ev,
8223
+ )
8224
+ raise KeyError(version)
8225
+
8226
+
8227
+ ########################################
8228
+ # ../remote/connection.py
8229
+
8230
+
8231
+ ##
8232
+
8233
+
8234
+ class PyremoteRemoteExecutionConnector:
8235
+ def __init__(
8236
+ self,
8237
+ *,
8238
+ spawning: RemoteSpawning,
8239
+ msh: ObjMarshalerManager,
8240
+ payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
8241
+ ) -> None:
8242
+ super().__init__()
8243
+
8244
+ self._spawning = spawning
8245
+ self._msh = msh
8246
+ self._payload_file = payload_file
8247
+
8248
+ #
8249
+
8250
+ @cached_nullary
8251
+ def _payload_src(self) -> str:
8252
+ return get_remote_payload_src(file=self._payload_file)
8253
+
8254
+ @cached_nullary
8255
+ def _remote_src(self) -> ta.Sequence[str]:
8256
+ return [
8257
+ self._payload_src(),
8258
+ '_remote_execution_main()',
8259
+ ]
8260
+
8261
+ @cached_nullary
8262
+ def _spawn_src(self) -> str:
8263
+ return pyremote_build_bootstrap_cmd(__package__ or 'manage')
8264
+
8265
+ #
8266
+
8267
+ @contextlib.asynccontextmanager
8268
+ async def connect(
8269
+ self,
8270
+ tgt: RemoteSpawning.Target,
8271
+ bs: MainBootstrap,
8272
+ ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
8273
+ spawn_src = self._spawn_src()
8274
+ remote_src = self._remote_src()
8275
+
8276
+ async with self._spawning.spawn(
8277
+ tgt,
8278
+ spawn_src,
8279
+ debug=bs.main_config.debug,
8280
+ ) as proc:
8281
+ res = await PyremoteBootstrapDriver( # noqa
8282
+ remote_src,
8283
+ PyremoteBootstrapOptions(
8284
+ debug=bs.main_config.debug,
8285
+ ),
8286
+ ).async_run(
8287
+ proc.stdout,
8288
+ proc.stdin,
8289
+ )
8290
+
8291
+ chan = RemoteChannelImpl(
8292
+ proc.stdout,
8293
+ proc.stdin,
8294
+ msh=self._msh,
8295
+ )
8296
+
8297
+ await chan.send_obj(bs)
8298
+
8299
+ rce: RemoteCommandExecutor
8300
+ async with aclosing(RemoteCommandExecutor(chan)) as rce:
8301
+ await rce.start()
8302
+
8303
+ yield rce
8304
+
8305
+
8306
+ ##
8307
+
8308
+
8309
+ class InProcessRemoteExecutionConnector:
8310
+ def __init__(
8311
+ self,
8312
+ *,
8313
+ msh: ObjMarshalerManager,
8314
+ local_executor: LocalCommandExecutor,
8315
+ ) -> None:
8316
+ super().__init__()
8317
+
8318
+ self._msh = msh
8319
+ self._local_executor = local_executor
8320
+
8321
+ @contextlib.asynccontextmanager
8322
+ async def connect(self) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
8323
+ r0, w0 = asyncio_create_bytes_channel()
8324
+ r1, w1 = asyncio_create_bytes_channel()
8325
+
8326
+ remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
8327
+ local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
6965
8328
 
6966
- async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
6967
- return [ev for e, ev in await self.exe_versions()]
8329
+ rch = _RemoteCommandHandler(
8330
+ remote_chan,
8331
+ self._local_executor,
8332
+ )
8333
+ rch_task = asyncio.create_task(rch.run()) # noqa
8334
+ try:
8335
+ rce: RemoteCommandExecutor
8336
+ async with aclosing(RemoteCommandExecutor(local_chan)) as rce:
8337
+ await rce.start()
6968
8338
 
6969
- async def get_installed_version(self, version: InterpVersion) -> Interp:
6970
- for e, ev in await self.exe_versions():
6971
- if ev != version:
6972
- continue
6973
- return Interp(
6974
- exe=e,
6975
- version=ev,
6976
- )
6977
- raise KeyError(version)
8339
+ yield rce
8340
+
8341
+ finally:
8342
+ rch.stop()
8343
+ await rch_task
6978
8344
 
6979
8345
 
6980
8346
  ########################################
6981
- # ../remote/inject.py
8347
+ # ../system/inject.py
6982
8348
 
6983
8349
 
6984
- def bind_remote(
8350
+ def bind_system(
6985
8351
  *,
6986
- remote_config: RemoteConfig,
8352
+ system_config: SystemConfig,
6987
8353
  ) -> InjectorBindings:
6988
8354
  lst: ta.List[InjectorBindingOrBindings] = [
6989
- inj.bind(remote_config),
6990
-
6991
- inj.bind(SubprocessRemoteSpawning, singleton=True),
6992
- inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
8355
+ inj.bind(system_config),
6993
8356
  ]
6994
8357
 
6995
8358
  #
6996
8359
 
6997
- if remote_config.use_in_process_remote_executor:
8360
+ platform = system_config.platform or detect_system_platform()
8361
+ lst.append(inj.bind(platform, key=Platform))
8362
+
8363
+ #
8364
+
8365
+ if isinstance(platform, AmazonLinuxPlatform):
6998
8366
  lst.extend([
6999
- inj.bind(InProcessRemoteExecutionConnector, singleton=True),
7000
- inj.bind(RemoteExecutionConnector, to_key=InProcessRemoteExecutionConnector),
8367
+ inj.bind(YumSystemPackageManager, singleton=True),
8368
+ inj.bind(SystemPackageManager, to_key=YumSystemPackageManager),
7001
8369
  ])
7002
- else:
8370
+
8371
+ elif isinstance(platform, LinuxPlatform):
8372
+ lst.extend([
8373
+ inj.bind(AptSystemPackageManager, singleton=True),
8374
+ inj.bind(SystemPackageManager, to_key=AptSystemPackageManager),
8375
+ ])
8376
+
8377
+ elif isinstance(platform, DarwinPlatform):
7003
8378
  lst.extend([
7004
- inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
7005
- inj.bind(RemoteExecutionConnector, to_key=PyremoteRemoteExecutionConnector),
8379
+ inj.bind(BrewSystemPackageManager, singleton=True),
8380
+ inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
7006
8381
  ])
7007
8382
 
7008
8383
  #
7009
8384
 
7010
- if (pf := remote_config.payload_file) is not None:
7011
- lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
8385
+ lst.extend([
8386
+ bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
8387
+ ])
7012
8388
 
7013
8389
  #
7014
8390
 
@@ -7114,189 +8490,248 @@ DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
7114
8490
 
7115
8491
 
7116
8492
  ########################################
7117
- # ../commands/interp.py
8493
+ # ../remote/inject.py
7118
8494
 
7119
8495
 
7120
- ##
8496
+ def bind_remote(
8497
+ *,
8498
+ remote_config: RemoteConfig,
8499
+ ) -> InjectorBindings:
8500
+ lst: ta.List[InjectorBindingOrBindings] = [
8501
+ inj.bind(remote_config),
7121
8502
 
8503
+ inj.bind(SubprocessRemoteSpawning, singleton=True),
8504
+ inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
7122
8505
 
7123
- @dc.dataclass(frozen=True)
7124
- class InterpCommand(Command['InterpCommand.Output']):
7125
- spec: str
7126
- install: bool = False
8506
+ inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
8507
+ inj.bind(InProcessRemoteExecutionConnector, singleton=True),
8508
+ ]
7127
8509
 
7128
- @dc.dataclass(frozen=True)
7129
- class Output(Command.Output):
7130
- exe: str
7131
- version: str
7132
- opts: InterpOpts
8510
+ #
8511
+
8512
+ if (pf := remote_config.payload_file) is not None:
8513
+ lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
7133
8514
 
8515
+ #
7134
8516
 
7135
- class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
7136
- async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
7137
- i = InterpSpecifier.parse(check.not_none(cmd.spec))
7138
- o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
7139
- return InterpCommand.Output(
7140
- exe=o.exe,
7141
- version=str(o.version.version),
7142
- opts=o.version.opts,
7143
- )
8517
+ return inj.as_bindings(*lst)
7144
8518
 
7145
8519
 
7146
8520
  ########################################
7147
- # ../commands/inject.py
8521
+ # ../targets/connection.py
7148
8522
 
7149
8523
 
7150
8524
  ##
7151
8525
 
7152
8526
 
7153
- def bind_command(
7154
- command_cls: ta.Type[Command],
7155
- executor_cls: ta.Optional[ta.Type[CommandExecutor]],
7156
- ) -> InjectorBindings:
7157
- lst: ta.List[InjectorBindingOrBindings] = [
7158
- inj.bind(CommandRegistration(command_cls), array=True),
7159
- ]
8527
+ class ManageTargetConnector(abc.ABC):
8528
+ @abc.abstractmethod
8529
+ def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
8530
+ raise NotImplementedError
7160
8531
 
7161
- if executor_cls is not None:
7162
- lst.extend([
7163
- inj.bind(executor_cls, singleton=True),
7164
- inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
7165
- ])
7166
8532
 
7167
- return inj.as_bindings(*lst)
8533
+ ##
7168
8534
 
7169
8535
 
7170
- ##
8536
+ ManageTargetConnectorMap = ta.NewType('ManageTargetConnectorMap', ta.Mapping[ta.Type[ManageTarget], ManageTargetConnector]) # noqa
7171
8537
 
7172
8538
 
7173
8539
  @dc.dataclass(frozen=True)
7174
- class _FactoryCommandExecutor(CommandExecutor):
7175
- factory: ta.Callable[[], CommandExecutor]
8540
+ class TypeSwitchedManageTargetConnector(ManageTargetConnector):
8541
+ connectors: ManageTargetConnectorMap
7176
8542
 
7177
- def execute(self, i: Command) -> ta.Awaitable[Command.Output]:
7178
- return self.factory().execute(i)
8543
+ def get_connector(self, ty: ta.Type[ManageTarget]) -> ManageTargetConnector:
8544
+ for k, v in self.connectors.items():
8545
+ if issubclass(ty, k):
8546
+ return v
8547
+ raise KeyError(ty)
8548
+
8549
+ def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
8550
+ return self.get_connector(type(tgt)).connect(tgt)
7179
8551
 
7180
8552
 
7181
8553
  ##
7182
8554
 
7183
8555
 
7184
- def bind_commands(
7185
- *,
7186
- main_config: MainConfig,
7187
- ) -> InjectorBindings:
7188
- lst: ta.List[InjectorBindingOrBindings] = [
7189
- inj.bind_array(CommandRegistration),
7190
- inj.bind_array_type(CommandRegistration, CommandRegistrations),
8556
+ @dc.dataclass(frozen=True)
8557
+ class LocalManageTargetConnector(ManageTargetConnector):
8558
+ _local_executor: LocalCommandExecutor
8559
+ _in_process_connector: InProcessRemoteExecutionConnector
8560
+ _pyremote_connector: PyremoteRemoteExecutionConnector
8561
+ _bootstrap: MainBootstrap
7191
8562
 
7192
- inj.bind_array(CommandExecutorRegistration),
7193
- inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
8563
+ @contextlib.asynccontextmanager
8564
+ async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
8565
+ lmt = check.isinstance(tgt, LocalManageTarget)
7194
8566
 
7195
- inj.bind(build_command_name_map, singleton=True),
7196
- ]
8567
+ if isinstance(lmt, InProcessManageTarget):
8568
+ imt = check.isinstance(lmt, InProcessManageTarget)
7197
8569
 
7198
- #
8570
+ if imt.mode == InProcessManageTarget.Mode.DIRECT:
8571
+ yield self._local_executor
7199
8572
 
7200
- def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
7201
- return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
8573
+ elif imt.mode == InProcessManageTarget.Mode.FAKE_REMOTE:
8574
+ async with self._in_process_connector.connect() as rce:
8575
+ yield rce
7202
8576
 
7203
- lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
8577
+ else:
8578
+ raise TypeError(imt.mode)
8579
+
8580
+ elif isinstance(lmt, SubprocessManageTarget):
8581
+ async with self._pyremote_connector.connect(
8582
+ RemoteSpawning.Target(
8583
+ python=lmt.python,
8584
+ ),
8585
+ self._bootstrap,
8586
+ ) as rce:
8587
+ yield rce
7204
8588
 
7205
- #
8589
+ else:
8590
+ raise TypeError(lmt)
7206
8591
 
7207
- def provide_command_executor_map(
7208
- injector: Injector,
7209
- crs: CommandExecutorRegistrations,
7210
- ) -> CommandExecutorMap:
7211
- dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
7212
8592
 
7213
- cr: CommandExecutorRegistration
7214
- for cr in crs:
7215
- if cr.command_cls in dct:
7216
- raise KeyError(cr.command_cls)
8593
+ ##
7217
8594
 
7218
- factory = functools.partial(injector.provide, cr.executor_cls)
7219
- if main_config.debug:
7220
- ce = factory()
7221
- else:
7222
- ce = _FactoryCommandExecutor(factory)
7223
8595
 
7224
- dct[cr.command_cls] = ce
8596
+ @dc.dataclass(frozen=True)
8597
+ class DockerManageTargetConnector(ManageTargetConnector):
8598
+ _pyremote_connector: PyremoteRemoteExecutionConnector
8599
+ _bootstrap: MainBootstrap
7225
8600
 
7226
- return CommandExecutorMap(dct)
8601
+ @contextlib.asynccontextmanager
8602
+ async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
8603
+ dmt = check.isinstance(tgt, DockerManageTarget)
8604
+
8605
+ sh_parts: ta.List[str] = ['docker']
8606
+ if dmt.image is not None:
8607
+ sh_parts.extend(['run', '-i', dmt.image])
8608
+ elif dmt.container_id is not None:
8609
+ sh_parts.extend(['exec', dmt.container_id])
8610
+ else:
8611
+ raise ValueError(dmt)
7227
8612
 
7228
- lst.extend([
7229
- inj.bind(provide_command_executor_map, singleton=True),
8613
+ async with self._pyremote_connector.connect(
8614
+ RemoteSpawning.Target(
8615
+ shell=' '.join(sh_parts),
8616
+ python=dmt.python,
8617
+ ),
8618
+ self._bootstrap,
8619
+ ) as rce:
8620
+ yield rce
7230
8621
 
7231
- inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
7232
- ])
7233
8622
 
7234
- #
8623
+ ##
7235
8624
 
7236
- command_cls: ta.Any
7237
- executor_cls: ta.Any
7238
- for command_cls, executor_cls in [
7239
- (SubprocessCommand, SubprocessCommandExecutor),
7240
- (InterpCommand, InterpCommandExecutor),
7241
- ]:
7242
- lst.append(bind_command(command_cls, executor_cls))
7243
8625
 
7244
- #
8626
+ @dc.dataclass(frozen=True)
8627
+ class SshManageTargetConnector(ManageTargetConnector):
8628
+ _pyremote_connector: PyremoteRemoteExecutionConnector
8629
+ _bootstrap: MainBootstrap
7245
8630
 
7246
- return inj.as_bindings(*lst)
8631
+ @contextlib.asynccontextmanager
8632
+ async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
8633
+ smt = check.isinstance(tgt, SshManageTarget)
8634
+
8635
+ sh_parts: ta.List[str] = ['ssh']
8636
+ if smt.key_file is not None:
8637
+ sh_parts.extend(['-i', smt.key_file])
8638
+ addr = check.not_none(smt.host)
8639
+ if smt.username is not None:
8640
+ addr = f'{smt.username}@{addr}'
8641
+ sh_parts.append(addr)
8642
+
8643
+ async with self._pyremote_connector.connect(
8644
+ RemoteSpawning.Target(
8645
+ shell=' '.join(sh_parts),
8646
+ shell_quote=True,
8647
+ python=smt.python,
8648
+ ),
8649
+ self._bootstrap,
8650
+ ) as rce:
8651
+ yield rce
7247
8652
 
7248
8653
 
7249
8654
  ########################################
7250
- # ../deploy/inject.py
8655
+ # ../deploy/interp.py
7251
8656
 
7252
8657
 
7253
- def bind_deploy(
7254
- ) -> InjectorBindings:
7255
- lst: ta.List[InjectorBindingOrBindings] = [
7256
- bind_command(DeployCommand, DeployCommandExecutor),
7257
- ]
8658
+ ##
7258
8659
 
7259
- return inj.as_bindings(*lst)
8660
+
8661
+ @dc.dataclass(frozen=True)
8662
+ class InterpCommand(Command['InterpCommand.Output']):
8663
+ spec: str
8664
+ install: bool = False
8665
+
8666
+ @dc.dataclass(frozen=True)
8667
+ class Output(Command.Output):
8668
+ exe: str
8669
+ version: str
8670
+ opts: InterpOpts
8671
+
8672
+
8673
+ class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
8674
+ async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
8675
+ i = InterpSpecifier.parse(check.not_none(cmd.spec))
8676
+ o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
8677
+ return InterpCommand.Output(
8678
+ exe=o.exe,
8679
+ version=str(o.version.version),
8680
+ opts=o.version.opts,
8681
+ )
7260
8682
 
7261
8683
 
7262
8684
  ########################################
7263
- # ../system/inject.py
8685
+ # ../targets/inject.py
7264
8686
 
7265
8687
 
7266
- def bind_system(
7267
- *,
7268
- system_config: SystemConfig,
7269
- ) -> InjectorBindings:
8688
+ def bind_targets() -> InjectorBindings:
7270
8689
  lst: ta.List[InjectorBindingOrBindings] = [
7271
- inj.bind(system_config),
8690
+ inj.bind(LocalManageTargetConnector, singleton=True),
8691
+ inj.bind(DockerManageTargetConnector, singleton=True),
8692
+ inj.bind(SshManageTargetConnector, singleton=True),
8693
+
8694
+ inj.bind(TypeSwitchedManageTargetConnector, singleton=True),
8695
+ inj.bind(ManageTargetConnector, to_key=TypeSwitchedManageTargetConnector),
7272
8696
  ]
7273
8697
 
7274
8698
  #
7275
8699
 
7276
- platform = system_config.platform or sys.platform
7277
- lst.append(inj.bind(platform, key=SystemPlatform))
8700
+ def provide_manage_target_connector_map(injector: Injector) -> ManageTargetConnectorMap:
8701
+ return ManageTargetConnectorMap({
8702
+ LocalManageTarget: injector[LocalManageTargetConnector],
8703
+ DockerManageTarget: injector[DockerManageTargetConnector],
8704
+ SshManageTarget: injector[SshManageTargetConnector],
8705
+ })
8706
+ lst.append(inj.bind(provide_manage_target_connector_map, singleton=True))
7278
8707
 
7279
8708
  #
7280
8709
 
7281
- if platform == 'linux':
7282
- lst.extend([
7283
- inj.bind(AptSystemPackageManager, singleton=True),
7284
- inj.bind(SystemPackageManager, to_key=AptSystemPackageManager),
7285
- ])
8710
+ return inj.as_bindings(*lst)
7286
8711
 
7287
- elif platform == 'darwin':
7288
- lst.extend([
7289
- inj.bind(BrewSystemPackageManager, singleton=True),
7290
- inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
7291
- ])
7292
8712
 
7293
- #
8713
+ ########################################
8714
+ # ../deploy/inject.py
7294
8715
 
7295
- lst.extend([
7296
- bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
7297
- ])
7298
8716
 
7299
- #
8717
+ def bind_deploy(
8718
+ *,
8719
+ deploy_config: DeployConfig,
8720
+ ) -> InjectorBindings:
8721
+ lst: ta.List[InjectorBindingOrBindings] = [
8722
+ inj.bind(deploy_config),
8723
+
8724
+ inj.bind(DeployAppManager, singleton=True),
8725
+ inj.bind(DeployGitManager, singleton=True),
8726
+ inj.bind(DeployVenvManager, singleton=True),
8727
+
8728
+ bind_command(DeployCommand, DeployCommandExecutor),
8729
+ bind_command(InterpCommand, InterpCommandExecutor),
8730
+ ]
8731
+
8732
+ if (dh := deploy_config.deploy_home) is not None:
8733
+ dh = os.path.abspath(os.path.expanduser(dh))
8734
+ lst.append(inj.bind(dh, key=DeployHome))
7300
8735
 
7301
8736
  return inj.as_bindings(*lst)
7302
8737
 
@@ -7310,9 +8745,13 @@ def bind_system(
7310
8745
 
7311
8746
  def bind_main(
7312
8747
  *,
7313
- main_config: MainConfig,
7314
- remote_config: RemoteConfig,
7315
- system_config: SystemConfig,
8748
+ main_config: MainConfig = MainConfig(),
8749
+
8750
+ deploy_config: DeployConfig = DeployConfig(),
8751
+ remote_config: RemoteConfig = RemoteConfig(),
8752
+ system_config: SystemConfig = SystemConfig(),
8753
+
8754
+ main_bootstrap: ta.Optional[MainBootstrap] = None,
7316
8755
  ) -> InjectorBindings:
7317
8756
  lst: ta.List[InjectorBindingOrBindings] = [
7318
8757
  inj.bind(main_config),
@@ -7321,7 +8760,9 @@ def bind_main(
7321
8760
  main_config=main_config,
7322
8761
  ),
7323
8762
 
7324
- bind_deploy(),
8763
+ bind_deploy(
8764
+ deploy_config=deploy_config,
8765
+ ),
7325
8766
 
7326
8767
  bind_remote(
7327
8768
  remote_config=remote_config,
@@ -7330,10 +8771,17 @@ def bind_main(
7330
8771
  bind_system(
7331
8772
  system_config=system_config,
7332
8773
  ),
8774
+
8775
+ bind_targets(),
7333
8776
  ]
7334
8777
 
7335
8778
  #
7336
8779
 
8780
+ if main_bootstrap is not None:
8781
+ lst.append(inj.bind(main_bootstrap))
8782
+
8783
+ #
8784
+
7337
8785
  def build_obj_marshaler_manager(insts: ObjMarshalerInstallers) -> ObjMarshalerManager:
7338
8786
  msh = ObjMarshalerManager()
7339
8787
  inst: ObjMarshalerInstaller
@@ -7363,8 +8811,12 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
7363
8811
 
7364
8812
  injector = inj.create_injector(bind_main( # noqa
7365
8813
  main_config=bs.main_config,
8814
+
8815
+ deploy_config=bs.deploy_config,
7366
8816
  remote_config=bs.remote_config,
7367
8817
  system_config=bs.system_config,
8818
+
8819
+ main_bootstrap=bs,
7368
8820
  ))
7369
8821
 
7370
8822
  return injector
@@ -7378,10 +8830,6 @@ class MainCli(ArgparseCli):
7378
8830
  @argparse_command(
7379
8831
  argparse_arg('--_payload-file'),
7380
8832
 
7381
- argparse_arg('-s', '--shell'),
7382
- argparse_arg('-q', '--shell-quote', action='store_true'),
7383
- argparse_arg('--python', default='python3'),
7384
-
7385
8833
  argparse_arg('--pycharm-debug-port', type=int),
7386
8834
  argparse_arg('--pycharm-debug-host'),
7387
8835
  argparse_arg('--pycharm-debug-version'),
@@ -7390,8 +8838,9 @@ class MainCli(ArgparseCli):
7390
8838
 
7391
8839
  argparse_arg('--debug', action='store_true'),
7392
8840
 
7393
- argparse_arg('--local', action='store_true'),
8841
+ argparse_arg('--deploy-home'),
7394
8842
 
8843
+ argparse_arg('target'),
7395
8844
  argparse_arg('command', nargs='+'),
7396
8845
  )
7397
8846
  async def run(self) -> None:
@@ -7402,6 +8851,10 @@ class MainCli(ArgparseCli):
7402
8851
  debug=bool(self.args.debug),
7403
8852
  ),
7404
8853
 
8854
+ deploy_config=DeployConfig(
8855
+ deploy_home=self.args.deploy_home,
8856
+ ),
8857
+
7405
8858
  remote_config=RemoteConfig(
7406
8859
  payload_file=self.args._payload_file, # noqa
7407
8860
 
@@ -7412,8 +8865,6 @@ class MainCli(ArgparseCli):
7412
8865
  ) if self.args.pycharm_debug_port is not None else None,
7413
8866
 
7414
8867
  timebomb_delay_s=self.args.remote_timebomb_delay_s,
7415
-
7416
- use_in_process_remote_executor=True,
7417
8868
  ),
7418
8869
  )
7419
8870
 
@@ -7427,6 +8878,11 @@ class MainCli(ArgparseCli):
7427
8878
 
7428
8879
  msh = injector[ObjMarshalerManager]
7429
8880
 
8881
+ ts = self.args.target
8882
+ if not ts.startswith('{'):
8883
+ ts = json.dumps({ts: {}})
8884
+ tgt: ManageTarget = msh.unmarshal_obj(json.loads(ts), ManageTarget)
8885
+
7430
8886
  cmds: ta.List[Command] = []
7431
8887
  cmd: Command
7432
8888
  for c in self.args.command:
@@ -7437,21 +8893,7 @@ class MainCli(ArgparseCli):
7437
8893
 
7438
8894
  #
7439
8895
 
7440
- async with contextlib.AsyncExitStack() as es:
7441
- ce: CommandExecutor
7442
-
7443
- if self.args.local:
7444
- ce = injector[LocalCommandExecutor]
7445
-
7446
- else:
7447
- tgt = RemoteSpawning.Target(
7448
- shell=self.args.shell,
7449
- shell_quote=self.args.shell_quote,
7450
- python=self.args.python,
7451
- )
7452
-
7453
- ce = await es.enter_async_context(injector[RemoteExecutionConnector].connect(tgt, bs)) # noqa
7454
-
8896
+ async with injector[ManageTargetConnector].connect(tgt) as ce:
7455
8897
  async def run_command(cmd: Command) -> None:
7456
8898
  res = await ce.try_execute(
7457
8899
  cmd,