##// END OF EJS Templates
automation: correct the path separator in LIBPATH on Windows...
Matt Harbison -
r42804:862f6bdd default
parent child Browse files
Show More
@@ -1,292 +1,292 b''
1 # windows.py - Automation specific to Windows
1 # windows.py - Automation specific to Windows
2 #
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # no-check-code because Python 3 native.
8 # no-check-code because Python 3 native.
9
9
10 import os
10 import os
11 import pathlib
11 import pathlib
12 import re
12 import re
13 import subprocess
13 import subprocess
14 import tempfile
14 import tempfile
15
15
16 from .winrm import (
16 from .winrm import (
17 run_powershell,
17 run_powershell,
18 )
18 )
19
19
20
20
21 # PowerShell commands to activate a Visual Studio 2008 environment.
21 # PowerShell commands to activate a Visual Studio 2008 environment.
22 # This is essentially a port of vcvarsall.bat to PowerShell.
22 # This is essentially a port of vcvarsall.bat to PowerShell.
23 ACTIVATE_VC9_AMD64 = r'''
23 ACTIVATE_VC9_AMD64 = r'''
24 Write-Output "activating Visual Studio 2008 environment for AMD64"
24 Write-Output "activating Visual Studio 2008 environment for AMD64"
25 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
25 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
26 $Env:VCINSTALLDIR = "${root}\VC\"
26 $Env:VCINSTALLDIR = "${root}\VC\"
27 $Env:WindowsSdkDir = "${root}\WinSDK\"
27 $Env:WindowsSdkDir = "${root}\WinSDK\"
28 $Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
28 $Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
29 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
29 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
30 $Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
30 $Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
31 $Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
31 $Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
32 '''.lstrip()
32 '''.lstrip()
33
33
34 ACTIVATE_VC9_X86 = r'''
34 ACTIVATE_VC9_X86 = r'''
35 Write-Output "activating Visual Studio 2008 environment for x86"
35 Write-Output "activating Visual Studio 2008 environment for x86"
36 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
36 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
37 $Env:VCINSTALLDIR = "${root}\VC\"
37 $Env:VCINSTALLDIR = "${root}\VC\"
38 $Env:WindowsSdkDir = "${root}\WinSDK\"
38 $Env:WindowsSdkDir = "${root}\WinSDK\"
39 $Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
39 $Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
40 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
40 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
41 $Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
41 $Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
42 $Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib:$Env:LIBPATH"
42 $Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib;$Env:LIBPATH"
43 '''.lstrip()
43 '''.lstrip()
44
44
45 HG_PURGE = r'''
45 HG_PURGE = r'''
46 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
46 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
47 Set-Location C:\hgdev\src
47 Set-Location C:\hgdev\src
48 hg.exe --config extensions.purge= purge --all
48 hg.exe --config extensions.purge= purge --all
49 if ($LASTEXITCODE -ne 0) {
49 if ($LASTEXITCODE -ne 0) {
50 throw "process exited non-0: $LASTEXITCODE"
50 throw "process exited non-0: $LASTEXITCODE"
51 }
51 }
52 Write-Output "purged Mercurial repo"
52 Write-Output "purged Mercurial repo"
53 '''
53 '''
54
54
55 HG_UPDATE_CLEAN = r'''
55 HG_UPDATE_CLEAN = r'''
56 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
56 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
57 Set-Location C:\hgdev\src
57 Set-Location C:\hgdev\src
58 hg.exe --config extensions.purge= purge --all
58 hg.exe --config extensions.purge= purge --all
59 if ($LASTEXITCODE -ne 0) {{
59 if ($LASTEXITCODE -ne 0) {{
60 throw "process exited non-0: $LASTEXITCODE"
60 throw "process exited non-0: $LASTEXITCODE"
61 }}
61 }}
62 hg.exe update -C {revision}
62 hg.exe update -C {revision}
63 if ($LASTEXITCODE -ne 0) {{
63 if ($LASTEXITCODE -ne 0) {{
64 throw "process exited non-0: $LASTEXITCODE"
64 throw "process exited non-0: $LASTEXITCODE"
65 }}
65 }}
66 hg.exe log -r .
66 hg.exe log -r .
67 Write-Output "updated Mercurial working directory to {revision}"
67 Write-Output "updated Mercurial working directory to {revision}"
68 '''.lstrip()
68 '''.lstrip()
69
69
70 BUILD_INNO = r'''
70 BUILD_INNO = r'''
71 Set-Location C:\hgdev\src
71 Set-Location C:\hgdev\src
72 $python = "C:\hgdev\python27-{arch}\python.exe"
72 $python = "C:\hgdev\python27-{arch}\python.exe"
73 C:\hgdev\python37-x64\python.exe contrib\packaging\inno\build.py --python $python
73 C:\hgdev\python37-x64\python.exe contrib\packaging\inno\build.py --python $python
74 if ($LASTEXITCODE -ne 0) {{
74 if ($LASTEXITCODE -ne 0) {{
75 throw "process exited non-0: $LASTEXITCODE"
75 throw "process exited non-0: $LASTEXITCODE"
76 }}
76 }}
77 '''.lstrip()
77 '''.lstrip()
78
78
79 BUILD_WHEEL = r'''
79 BUILD_WHEEL = r'''
80 Set-Location C:\hgdev\src
80 Set-Location C:\hgdev\src
81 C:\hgdev\python27-{arch}\Scripts\pip.exe wheel --wheel-dir dist .
81 C:\hgdev\python27-{arch}\Scripts\pip.exe wheel --wheel-dir dist .
82 if ($LASTEXITCODE -ne 0) {{
82 if ($LASTEXITCODE -ne 0) {{
83 throw "process exited non-0: $LASTEXITCODE"
83 throw "process exited non-0: $LASTEXITCODE"
84 }}
84 }}
85 '''
85 '''
86
86
87 BUILD_WIX = r'''
87 BUILD_WIX = r'''
88 Set-Location C:\hgdev\src
88 Set-Location C:\hgdev\src
89 $python = "C:\hgdev\python27-{arch}\python.exe"
89 $python = "C:\hgdev\python27-{arch}\python.exe"
90 C:\hgdev\python37-x64\python.exe contrib\packaging\wix\build.py --python $python {extra_args}
90 C:\hgdev\python37-x64\python.exe contrib\packaging\wix\build.py --python $python {extra_args}
91 if ($LASTEXITCODE -ne 0) {{
91 if ($LASTEXITCODE -ne 0) {{
92 throw "process exited non-0: $LASTEXITCODE"
92 throw "process exited non-0: $LASTEXITCODE"
93 }}
93 }}
94 '''
94 '''
95
95
96 RUN_TESTS = r'''
96 RUN_TESTS = r'''
97 C:\hgdev\MinGW\msys\1.0\bin\sh.exe --login -c "cd /c/hgdev/src/tests && /c/hgdev/{python_path}/python.exe run-tests.py {test_flags}"
97 C:\hgdev\MinGW\msys\1.0\bin\sh.exe --login -c "cd /c/hgdev/src/tests && /c/hgdev/{python_path}/python.exe run-tests.py {test_flags}"
98 if ($LASTEXITCODE -ne 0) {{
98 if ($LASTEXITCODE -ne 0) {{
99 throw "process exited non-0: $LASTEXITCODE"
99 throw "process exited non-0: $LASTEXITCODE"
100 }}
100 }}
101 '''
101 '''
102
102
103
103
104 def get_vc_prefix(arch):
104 def get_vc_prefix(arch):
105 if arch == 'x86':
105 if arch == 'x86':
106 return ACTIVATE_VC9_X86
106 return ACTIVATE_VC9_X86
107 elif arch == 'x64':
107 elif arch == 'x64':
108 return ACTIVATE_VC9_AMD64
108 return ACTIVATE_VC9_AMD64
109 else:
109 else:
110 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
110 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
111
111
112
112
113 def fix_authorized_keys_permissions(winrm_client, path):
113 def fix_authorized_keys_permissions(winrm_client, path):
114 commands = [
114 commands = [
115 '$ErrorActionPreference = "Stop"',
115 '$ErrorActionPreference = "Stop"',
116 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
116 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
117 r'icacls %s /remove:g "NT Service\sshd"' % path,
117 r'icacls %s /remove:g "NT Service\sshd"' % path,
118 ]
118 ]
119
119
120 run_powershell(winrm_client, '\n'.join(commands))
120 run_powershell(winrm_client, '\n'.join(commands))
121
121
122
122
123 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
123 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
124 """Synchronize local Mercurial repo to remote EC2 instance."""
124 """Synchronize local Mercurial repo to remote EC2 instance."""
125
125
126 winrm_client = ec2_instance.winrm_client
126 winrm_client = ec2_instance.winrm_client
127
127
128 with tempfile.TemporaryDirectory() as temp_dir:
128 with tempfile.TemporaryDirectory() as temp_dir:
129 temp_dir = pathlib.Path(temp_dir)
129 temp_dir = pathlib.Path(temp_dir)
130
130
131 ssh_dir = temp_dir / '.ssh'
131 ssh_dir = temp_dir / '.ssh'
132 ssh_dir.mkdir()
132 ssh_dir.mkdir()
133 ssh_dir.chmod(0o0700)
133 ssh_dir.chmod(0o0700)
134
134
135 # Generate SSH key to use for communication.
135 # Generate SSH key to use for communication.
136 subprocess.run([
136 subprocess.run([
137 'ssh-keygen', '-t', 'rsa', '-b', '4096', '-N', '',
137 'ssh-keygen', '-t', 'rsa', '-b', '4096', '-N', '',
138 '-f', str(ssh_dir / 'id_rsa')],
138 '-f', str(ssh_dir / 'id_rsa')],
139 check=True, capture_output=True)
139 check=True, capture_output=True)
140
140
141 # Add it to ~/.ssh/authorized_keys on remote.
141 # Add it to ~/.ssh/authorized_keys on remote.
142 # This assumes the file doesn't already exist.
142 # This assumes the file doesn't already exist.
143 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
143 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
144 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
144 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
145 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
145 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
146 fix_authorized_keys_permissions(winrm_client, authorized_keys)
146 fix_authorized_keys_permissions(winrm_client, authorized_keys)
147
147
148 public_ip = ec2_instance.public_ip_address
148 public_ip = ec2_instance.public_ip_address
149
149
150 ssh_config = temp_dir / '.ssh' / 'config'
150 ssh_config = temp_dir / '.ssh' / 'config'
151
151
152 with open(ssh_config, 'w', encoding='utf-8') as fh:
152 with open(ssh_config, 'w', encoding='utf-8') as fh:
153 fh.write('Host %s\n' % public_ip)
153 fh.write('Host %s\n' % public_ip)
154 fh.write(' User Administrator\n')
154 fh.write(' User Administrator\n')
155 fh.write(' StrictHostKeyChecking no\n')
155 fh.write(' StrictHostKeyChecking no\n')
156 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
156 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
157 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
157 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
158
158
159 if not (hg_repo / '.hg').is_dir():
159 if not (hg_repo / '.hg').is_dir():
160 raise Exception('%s is not a Mercurial repository; '
160 raise Exception('%s is not a Mercurial repository; '
161 'synchronization not yet supported' % hg_repo)
161 'synchronization not yet supported' % hg_repo)
162
162
163 env = dict(os.environ)
163 env = dict(os.environ)
164 env['HGPLAIN'] = '1'
164 env['HGPLAIN'] = '1'
165 env['HGENCODING'] = 'utf-8'
165 env['HGENCODING'] = 'utf-8'
166
166
167 hg_bin = hg_repo / 'hg'
167 hg_bin = hg_repo / 'hg'
168
168
169 res = subprocess.run(
169 res = subprocess.run(
170 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
170 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
171 cwd=str(hg_repo), env=env, check=True, capture_output=True)
171 cwd=str(hg_repo), env=env, check=True, capture_output=True)
172
172
173 full_revision = res.stdout.decode('ascii')
173 full_revision = res.stdout.decode('ascii')
174
174
175 args = [
175 args = [
176 'python2.7', hg_bin,
176 'python2.7', hg_bin,
177 '--config', 'ui.ssh=ssh -F %s' % ssh_config,
177 '--config', 'ui.ssh=ssh -F %s' % ssh_config,
178 '--config', 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
178 '--config', 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
179 'push', '-f', '-r', full_revision,
179 'push', '-f', '-r', full_revision,
180 'ssh://%s/c:/hgdev/src' % public_ip,
180 'ssh://%s/c:/hgdev/src' % public_ip,
181 ]
181 ]
182
182
183 subprocess.run(args, cwd=str(hg_repo), env=env, check=True)
183 subprocess.run(args, cwd=str(hg_repo), env=env, check=True)
184
184
185 run_powershell(winrm_client,
185 run_powershell(winrm_client,
186 HG_UPDATE_CLEAN.format(revision=full_revision))
186 HG_UPDATE_CLEAN.format(revision=full_revision))
187
187
188 # TODO detect dirty local working directory and synchronize accordingly.
188 # TODO detect dirty local working directory and synchronize accordingly.
189
189
190
190
191 def purge_hg(winrm_client):
191 def purge_hg(winrm_client):
192 """Purge the Mercurial source repository on an EC2 instance."""
192 """Purge the Mercurial source repository on an EC2 instance."""
193 run_powershell(winrm_client, HG_PURGE)
193 run_powershell(winrm_client, HG_PURGE)
194
194
195
195
196 def find_latest_dist(winrm_client, pattern):
196 def find_latest_dist(winrm_client, pattern):
197 """Find path to newest file in dist/ directory matching a pattern."""
197 """Find path to newest file in dist/ directory matching a pattern."""
198
198
199 res = winrm_client.execute_ps(
199 res = winrm_client.execute_ps(
200 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
200 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
201 '| Sort-Object LastWriteTime -Descending '
201 '| Sort-Object LastWriteTime -Descending '
202 '| Select-Object -First 1\n'
202 '| Select-Object -First 1\n'
203 '$v.name' % pattern
203 '$v.name' % pattern
204 )
204 )
205 return res[0]
205 return res[0]
206
206
207
207
208 def copy_latest_dist(winrm_client, pattern, dest_path):
208 def copy_latest_dist(winrm_client, pattern, dest_path):
209 """Copy latest file matching pattern in dist/ directory.
209 """Copy latest file matching pattern in dist/ directory.
210
210
211 Given a WinRM client and a file pattern, find the latest file on the remote
211 Given a WinRM client and a file pattern, find the latest file on the remote
212 matching that pattern and copy it to the ``dest_path`` directory on the
212 matching that pattern and copy it to the ``dest_path`` directory on the
213 local machine.
213 local machine.
214 """
214 """
215 latest = find_latest_dist(winrm_client, pattern)
215 latest = find_latest_dist(winrm_client, pattern)
216 source = r'C:\hgdev\src\dist\%s' % latest
216 source = r'C:\hgdev\src\dist\%s' % latest
217 dest = dest_path / latest
217 dest = dest_path / latest
218 print('copying %s to %s' % (source, dest))
218 print('copying %s to %s' % (source, dest))
219 winrm_client.fetch(source, str(dest))
219 winrm_client.fetch(source, str(dest))
220
220
221
221
222 def build_inno_installer(winrm_client, arch: str, dest_path: pathlib.Path,
222 def build_inno_installer(winrm_client, arch: str, dest_path: pathlib.Path,
223 version=None):
223 version=None):
224 """Build the Inno Setup installer on a remote machine.
224 """Build the Inno Setup installer on a remote machine.
225
225
226 Using a WinRM client, remote commands are executed to build
226 Using a WinRM client, remote commands are executed to build
227 a Mercurial Inno Setup installer.
227 a Mercurial Inno Setup installer.
228 """
228 """
229 print('building Inno Setup installer for %s' % arch)
229 print('building Inno Setup installer for %s' % arch)
230
230
231 extra_args = []
231 extra_args = []
232 if version:
232 if version:
233 extra_args.extend(['--version', version])
233 extra_args.extend(['--version', version])
234
234
235 ps = get_vc_prefix(arch) + BUILD_INNO.format(arch=arch,
235 ps = get_vc_prefix(arch) + BUILD_INNO.format(arch=arch,
236 extra_args=' '.join(extra_args))
236 extra_args=' '.join(extra_args))
237 run_powershell(winrm_client, ps)
237 run_powershell(winrm_client, ps)
238 copy_latest_dist(winrm_client, '*.exe', dest_path)
238 copy_latest_dist(winrm_client, '*.exe', dest_path)
239
239
240
240
241 def build_wheel(winrm_client, arch: str, dest_path: pathlib.Path):
241 def build_wheel(winrm_client, arch: str, dest_path: pathlib.Path):
242 """Build Python wheels on a remote machine.
242 """Build Python wheels on a remote machine.
243
243
244 Using a WinRM client, remote commands are executed to build a Python wheel
244 Using a WinRM client, remote commands are executed to build a Python wheel
245 for Mercurial.
245 for Mercurial.
246 """
246 """
247 print('Building Windows wheel for %s' % arch)
247 print('Building Windows wheel for %s' % arch)
248 ps = get_vc_prefix(arch) + BUILD_WHEEL.format(arch=arch)
248 ps = get_vc_prefix(arch) + BUILD_WHEEL.format(arch=arch)
249 run_powershell(winrm_client, ps)
249 run_powershell(winrm_client, ps)
250 copy_latest_dist(winrm_client, '*.whl', dest_path)
250 copy_latest_dist(winrm_client, '*.whl', dest_path)
251
251
252
252
253 def build_wix_installer(winrm_client, arch: str, dest_path: pathlib.Path,
253 def build_wix_installer(winrm_client, arch: str, dest_path: pathlib.Path,
254 version=None):
254 version=None):
255 """Build the WiX installer on a remote machine.
255 """Build the WiX installer on a remote machine.
256
256
257 Using a WinRM client, remote commands are executed to build a WiX installer.
257 Using a WinRM client, remote commands are executed to build a WiX installer.
258 """
258 """
259 print('Building WiX installer for %s' % arch)
259 print('Building WiX installer for %s' % arch)
260 extra_args = []
260 extra_args = []
261 if version:
261 if version:
262 extra_args.extend(['--version', version])
262 extra_args.extend(['--version', version])
263
263
264 ps = get_vc_prefix(arch) + BUILD_WIX.format(arch=arch,
264 ps = get_vc_prefix(arch) + BUILD_WIX.format(arch=arch,
265 extra_args=' '.join(extra_args))
265 extra_args=' '.join(extra_args))
266 run_powershell(winrm_client, ps)
266 run_powershell(winrm_client, ps)
267 copy_latest_dist(winrm_client, '*.msi', dest_path)
267 copy_latest_dist(winrm_client, '*.msi', dest_path)
268
268
269
269
270 def run_tests(winrm_client, python_version, arch, test_flags=''):
270 def run_tests(winrm_client, python_version, arch, test_flags=''):
271 """Run tests on a remote Windows machine.
271 """Run tests on a remote Windows machine.
272
272
273 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
273 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
274 ``arch`` is ``x86`` or ``x64``.
274 ``arch`` is ``x86`` or ``x64``.
275 ``test_flags`` is a str representing extra arguments to pass to
275 ``test_flags`` is a str representing extra arguments to pass to
276 ``run-tests.py``.
276 ``run-tests.py``.
277 """
277 """
278 if not re.match(r'\d\.\d', python_version):
278 if not re.match(r'\d\.\d', python_version):
279 raise ValueError(r'python_version must be \d.\d; got %s' %
279 raise ValueError(r'python_version must be \d.\d; got %s' %
280 python_version)
280 python_version)
281
281
282 if arch not in ('x86', 'x64'):
282 if arch not in ('x86', 'x64'):
283 raise ValueError('arch must be x86 or x64; got %s' % arch)
283 raise ValueError('arch must be x86 or x64; got %s' % arch)
284
284
285 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
285 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
286
286
287 ps = RUN_TESTS.format(
287 ps = RUN_TESTS.format(
288 python_path=python_path,
288 python_path=python_path,
289 test_flags=test_flags or '',
289 test_flags=test_flags or '',
290 )
290 )
291
291
292 run_powershell(winrm_client, ps)
292 run_powershell(winrm_client, ps)
General Comments 0
You need to be logged in to leave comments. Login now