Add Windows live log streaming
This adds support for live log streaming on Windows hosts. The daemon is written in C#, but is structured exactly like the Python daemon for Posix, so that any changes to one can be easily translated to the other. Like the Python/Posix version, it is necessary to vendor some Powershell and C# code from Ansible and the ansible.windows collection. Minimal changes were made to these (in fact, much more minimal than the changes to the command.py module for the Posix version). All changes involve the word "Zuul" for easy identification. Most of the work to write out the console log files is in a new file, Ansible.Zuul.Win.Common.cs which appears only in Zuul. Becasue it's possible to run the Posix log streaming daemon in WSL, and it's also possible to run both "win_command" and "command" tasks on the same host, outside and inside of WSL, the Windows log streaming daemon (win_zuul_console) runs on a different port, so that both may be running on the same host at the same time. This is more straightforward than trying to have both daemons be aware of the file paths of the other. I did make an attempt to create a test environment with wine, dotnet, and powershell all running under linux that we could use in the gate, however something about how Ansible links the C# modules is different enough to render that nonfunctional. Perhaps someday it will work; until then, we will have to use our best efforts to maintain this and manually test with a Windows VM. The procedure for running the remote tests is the same for Windows as it is for Posix. A copy of the "command" remote streaming test is added for windows, but disabled in the gate due to test resources. Change-Id: I35c220c1774b1943165075c3730236c78b95b18d
This commit is contained in:
parent
dd065c17a7
commit
63e3552e9c
@ -214,14 +214,33 @@ See also the Ansible `Python 3 support page
|
||||
|
||||
.. _nodepool_console_streaming:
|
||||
|
||||
Log streaming
|
||||
Log Streaming
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The log streaming service enables Zuul to show the live status of
|
||||
long-running ``shell`` or ``command`` tasks. The server side is setup
|
||||
by the ``zuul_console:`` task built-in to Zuul's Ansible installation.
|
||||
The executor requires the ability to communicate with this server on
|
||||
the job nodes via port ``19885`` for this to work.
|
||||
long-running ``shell``, ``command``, ``win_shell``, or ``win_command``
|
||||
tasks.
|
||||
|
||||
Log streaming is available on both Posix and Windows based hosts. The
|
||||
two systems operate in the same way with some minor differences. They
|
||||
are compatible, and in the case where a Windows host runs Windows
|
||||
Subsystem for Linux (WSL), they may operate at the same time.
|
||||
|
||||
For Kubernetes-based job nodes the connection from the executor to the
|
||||
log streaming daemon is established by using ``kubectl port-forward``
|
||||
to forward a local port to the appropriate port on the pod containing
|
||||
the job node. If the Kubernetes user is not bound to a role that has
|
||||
authorization for port-forwarding, this will prevent connection to
|
||||
the daemon.
|
||||
|
||||
Posix Log Streaming
|
||||
+++++++++++++++++++
|
||||
|
||||
The Posix log streaming service handles output from ``shell`` and
|
||||
``command`` tasks. The server side is setup by the ``zuul_console:``
|
||||
task built-in to Zuul's Ansible installation. The executor requires
|
||||
the ability to communicate with this server on the job nodes via port
|
||||
``19885`` for this to work.
|
||||
|
||||
The log streaming service spools command output via files on the job
|
||||
node in the format ``/tmp/console-<uuid>-<task_id>-<host>.log``. By
|
||||
@ -241,19 +260,43 @@ some other reason, the command to clean these spool files will not be
|
||||
processed and they may be left behind; on an ephemeral node this is
|
||||
not usually a problem, but on a static node these files will persist.
|
||||
|
||||
In this situation, Zuul can be instructed to not to create any spool
|
||||
files for ``shell`` and ``command`` tasks via setting
|
||||
``zuul_console_disabled: True`` (usually via a global host variable in
|
||||
inventory). Live streaming of ``shell`` and ``command`` calls will of
|
||||
In this situation, Zuul can be instructed not to create any spool
|
||||
files for ``shell``, ``command``, ``win_shell``, or ``win_command``
|
||||
tasks by setting ``zuul_console_disabled: True`` (usually via a global
|
||||
host variable in inventory). Live streaming of these tasks will of
|
||||
course be unavailable in this case, but no spool files will be
|
||||
created.
|
||||
|
||||
Windows Log Streaming
|
||||
+++++++++++++++++++++
|
||||
|
||||
The Windows log streaming service handles output from ``win_shell``
|
||||
and ``win_command`` tasks. The server side is setup by the
|
||||
``win_zuul_console:`` task built-in to Zuul's Ansible installation.
|
||||
The executor requires the ability to communicate with this server on
|
||||
the job nodes via port ``19886`` for this to work.
|
||||
|
||||
The log streaming service spools command output via files on the job
|
||||
node in the format ``C:/Users/All
|
||||
Users/Zuul/console-console-<uuid>-<task_id>-<host>.log``. By default,
|
||||
it will clean these files up automatically.
|
||||
|
||||
Occasionally, a streaming file may be left if a job is interrupted.
|
||||
These may be safely removed after a short period of inactivity.
|
||||
|
||||
If the executor is unable to reach port ``19886`` (for example due to
|
||||
firewall rules), or the ``win_zuul_console`` daemon can not be run for
|
||||
some other reason, the command to clean these spool files will not be
|
||||
processed and they may be left behind; on an ephemeral node this is
|
||||
not usually a problem, but on a static node these files will persist.
|
||||
|
||||
In this situation, Zuul can be instructed not to create any spool
|
||||
files for ``shell``, ``command``, ``win_shell``, or ``win_command``
|
||||
tasks by setting ``zuul_console_disabled: True`` (usually via a global
|
||||
host variable in inventory). Live streaming of these tasks will of
|
||||
course be unavailable in this case, but no spool files will be
|
||||
created.
|
||||
|
||||
For Kubernetes-based job nodes the connection from the executor to the
|
||||
``zuul_console`` daemon is established by using ``kubectl port-forward``
|
||||
to forward a local port to the appropriate port on the pod containing
|
||||
the job node. If the Kubernetes user is not bound to a role that has
|
||||
authorization for port-forwarding, this will prevent connection to
|
||||
the ``zuul_console`` daemon.
|
||||
|
||||
Web Server
|
||||
----------
|
||||
|
24
releasenotes/notes/windows-streaming-d5a2dae41b4c826d.yaml
Normal file
24
releasenotes/notes/windows-streaming-d5a2dae41b4c826d.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Live console log streaming is now available for ``win_shell` and
|
||||
``win_command`` tasks on Windows hosts.
|
||||
upgrade:
|
||||
- |
|
||||
Due to the addition of live console log streaming for Windows
|
||||
hosts, one of the following is necessary:
|
||||
|
||||
* Add the ``zuul_win_console:`` task to the first pre-run
|
||||
playbook in a base job for Windows hosts, and allow
|
||||
connections to port 19886 on the hosts. This will allow for
|
||||
automatic live log streaming in the same manner as under Posix
|
||||
systems.
|
||||
|
||||
* If the above is not practical on long-lived Windows hosts, set
|
||||
``zuul_console_disabled: True`` for those hosts. This will
|
||||
disable writing console output to spool files on the remote
|
||||
hosts which would otherwise not be deleted.
|
||||
|
||||
Note that the Posix and Windows log streaming servers operate on
|
||||
different ports (19885 and 19886 respectively) in order to allow
|
||||
both to coexist on the same host.
|
@ -1334,6 +1334,8 @@ class FakeNodepool(object):
|
||||
data['resources'] = self.resources
|
||||
if self.remote_ansible:
|
||||
data['connection_type'] = 'ssh'
|
||||
if os.environ.get("ZUUL_REMOTE_USER"):
|
||||
data['username'] = os.environ.get("ZUUL_REMOTE_USER")
|
||||
if 'fakeuser' in node_type:
|
||||
data['username'] = 'fakeuser'
|
||||
if 'windows' in node_type:
|
||||
|
183
tests/fixtures/config/remote-zuul-stream/git/org_project/playbooks/win-command.yaml
vendored
Normal file
183
tests/fixtures/config/remote-zuul-stream/git/org_project/playbooks/win-command.yaml
vendored
Normal file
@ -0,0 +1,183 @@
|
||||
- hosts: localhost
|
||||
tasks:
|
||||
- debug:
|
||||
msg: Ansible version={{ ansible_version.major }}.{{ ansible_version.minor }}
|
||||
|
||||
- hosts: all
|
||||
tasks:
|
||||
# This is a noop to make the task numbers line up with the console job.
|
||||
- name: "Noop"
|
||||
debug:
|
||||
msg: noop
|
||||
run_once: True
|
||||
|
||||
- name: Start zuul_console daemon
|
||||
win_zuul_console:
|
||||
port: "{{ test_console_port }}"
|
||||
|
||||
# This is checked in indexed JSON output so order is important
|
||||
- name: Output stream test
|
||||
win_shell: |
|
||||
echo "Standard output test {{ zuul.executor.src_root }}"
|
||||
[Console]::Error.WriteLine("Standard error test {{ zuul.executor.src_root }}")
|
||||
|
||||
- name: Create first file
|
||||
win_copy:
|
||||
content: "command test one\n"
|
||||
dest: "{{ ansible_user_dir }}/command_test_file1"
|
||||
- name: Create second file
|
||||
win_copy:
|
||||
content: "command test two\n"
|
||||
dest: "{{ ansible_user_dir }}/command_test_file2"
|
||||
- name: Show contents of first file
|
||||
win_command: "cmd /c type {{ ansible_user_dir }}\\command_test_file1"
|
||||
- name: Show contents of second file
|
||||
# We use a sleep here to ensure that we log even after
|
||||
# a period of no logging.
|
||||
win_shell: "sleep 6; cat {{ ansible_user_dir }}/command_test_file2"
|
||||
|
||||
# Test a task with a handler
|
||||
- name: Run a command with notifying a handler
|
||||
win_command: "cmd /c exit 0"
|
||||
notify: test handler
|
||||
|
||||
# Test cleanup task
|
||||
- name: Block with cleanup
|
||||
block:
|
||||
- name: Run a command
|
||||
win_command: "cmd /c exit 1"
|
||||
rescue:
|
||||
- name: Rescue task
|
||||
win_command: 'cmd /c echo This is a rescue task'
|
||||
always:
|
||||
- name: Always task
|
||||
win_command: 'cmd /c echo This is an always task'
|
||||
|
||||
- name: Skip command task
|
||||
win_command: "cmd /c exit 0"
|
||||
when: false
|
||||
|
||||
- name: Skip command task loop
|
||||
win_command: "cmd /c exit 0"
|
||||
when: false
|
||||
with_items:
|
||||
- failed_in_loop1
|
||||
- failed_in_loop2
|
||||
|
||||
- name: Set testfile path name for later test
|
||||
set_fact:
|
||||
testfile: "{{ ansible_user_dir }}\\testfile-{{ zuul.build }}"
|
||||
|
||||
handlers:
|
||||
- name: test handler
|
||||
win_command: 'cmd /c echo This is a handler'
|
||||
|
||||
- hosts: all
|
||||
strategy: free
|
||||
tasks:
|
||||
- name: Command task 1 within free strategy
|
||||
win_command: 'cmd /c echo First free task'
|
||||
- name: Command task 2 within free strategy
|
||||
win_command: 'cmd /c echo Second free task'
|
||||
|
||||
# Test a role that has an include_role
|
||||
- hosts: all
|
||||
strategy: linear
|
||||
roles:
|
||||
- win-include-a-role
|
||||
|
||||
- hosts: compute1
|
||||
tasks:
|
||||
- name: Single command
|
||||
win_command: 'cmd /c echo single'
|
||||
# Test commands within loops
|
||||
- name: Command with loop
|
||||
win_shell: |
|
||||
echo {{ item }}
|
||||
with_items:
|
||||
- item_in_loop1
|
||||
- item_in_loop2
|
||||
- name: Failing command with loop
|
||||
win_shell: |
|
||||
echo {{ item }}
|
||||
exit 1
|
||||
with_items:
|
||||
- failed_in_loop1
|
||||
- failed_in_loop2
|
||||
ignore_errors: True
|
||||
- name: Creates file that does not exist
|
||||
win_command: 'cmd /c "echo foo > {{ testfile }}"'
|
||||
args:
|
||||
creates: "{{ testfile }}"
|
||||
- name: Creates file that already exists
|
||||
win_command: 'cmd /c "echo foo > {{ testfile }}"'
|
||||
args:
|
||||
creates: "{{ testfile }}"
|
||||
|
||||
# Try transitive includes two different ways
|
||||
- hosts: compute1
|
||||
tasks:
|
||||
- include_role:
|
||||
name: win-include-echo-role
|
||||
vars:
|
||||
item: transitive-one
|
||||
- include_role:
|
||||
name: win-include-echo-role
|
||||
vars:
|
||||
item: transitive-two
|
||||
|
||||
- hosts: compute1
|
||||
roles:
|
||||
- role: win-include-echo-role
|
||||
item: transitive-three
|
||||
- role: win-include-echo-role
|
||||
item: transitive-four
|
||||
|
||||
- hosts: compute1
|
||||
tasks:
|
||||
- name: Command Not Found
|
||||
win_command: command-not-found
|
||||
failed_when: false
|
||||
|
||||
- hosts: compute1
|
||||
tasks:
|
||||
|
||||
- name: Debug raw variable in msg
|
||||
debug:
|
||||
msg: '{{ ansible_version }}'
|
||||
|
||||
- name: Debug raw variable in a loop
|
||||
debug:
|
||||
msg: '{{ ansible_version }}'
|
||||
loop:
|
||||
- 1
|
||||
- 2
|
||||
|
||||
- hosts: all
|
||||
tasks:
|
||||
- name: Clean up tmpfile
|
||||
win_file:
|
||||
path: "{{ testfile }}"
|
||||
state: absent
|
||||
# win_file may not be idempotent
|
||||
failed_when: false
|
||||
# Test large output
|
||||
- name: Output 70KB each to stdout and stderr
|
||||
# We use print (as opposed to write) so this uses buffered
|
||||
# output in order to encourage a deadlock.
|
||||
win_shell: python -c "import sys; print('x' * 70000, file=sys.stdout); print('x' * 70000, file=sys.stderr)"
|
||||
|
||||
- hosts: localhost
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Add a fake host
|
||||
add_host:
|
||||
hostname: fake
|
||||
ansible_host: notexisting.example.notexisting
|
||||
|
||||
- hosts: fake
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Skip a command on an unreachable host
|
||||
win_command: cmd /c echo nope
|
||||
when: false
|
2
tests/fixtures/config/remote-zuul-stream/git/org_project/roles/win-echo-role/tasks/main.yaml
vendored
Normal file
2
tests/fixtures/config/remote-zuul-stream/git/org_project/roles/win-echo-role/tasks/main.yaml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
- name: Echo message
|
||||
win_command: "cmd /c echo {{item}}"
|
@ -0,0 +1,14 @@
|
||||
# We've seen callback problems with shell tasks after included roles.
|
||||
- name: Emit a debug message
|
||||
include_role:
|
||||
name: debug-role
|
||||
|
||||
- name: Include role shell task
|
||||
win_shell: echo "This is a shell task after an included role"
|
||||
|
||||
- name: Include role command task
|
||||
win_command: cmd /c echo This is a command task after an included role
|
||||
|
||||
- name: Include role shell task delegate
|
||||
win_shell: echo "This is a shell task with delegate {{ inventory_hostname }}"
|
||||
delegate_to: controller
|
@ -0,0 +1,3 @@
|
||||
- name: Include echo role
|
||||
include_role:
|
||||
name: win-echo-role
|
9
tests/fixtures/fake_kubectl.sh
vendored
9
tests/fixtures/fake_kubectl.sh
vendored
@ -4,12 +4,15 @@
|
||||
# --kubeconfig=/tmp/tmppm0yyqvv/zuul-test/builds/c21fc1eb7e2c469cb4997d688252dc3c/work/.kube/config --context=zuul-ci-abcdefg:zuul-worker/ -n zuul-ci-abcdefg port-forward pod/fedora-abcdefg 37303:19885
|
||||
|
||||
# Get the last argument to the script
|
||||
arg=${@:$#}
|
||||
arg1=${@: -2}
|
||||
arg2=${@: -1}
|
||||
|
||||
# Split on the colon
|
||||
ports=(${arg//:/ })
|
||||
ports1=(${arg1//:/ })
|
||||
ports2=(${arg2//:/ })
|
||||
|
||||
echo "Forwarding from 127.0.0.1:${ports[0]} -> ${ports[1]}"
|
||||
echo "Forwarding from 127.0.0.1:${ports1[0]} -> ${ports1[1]}"
|
||||
echo "Forwarding from 127.0.0.1:${ports2[0]} -> ${ports2[1]}"
|
||||
|
||||
while true; do
|
||||
sleep 5
|
||||
|
@ -462,6 +462,139 @@ class FunctionalZuulStreamMixIn:
|
||||
with open(path) as f:
|
||||
self.log.debug(f.read())
|
||||
|
||||
@skip("Windows unavailable in gate")
|
||||
def test_win_command(self):
|
||||
self.fake_nodepool.shell_type = 'cmd'
|
||||
job = self._run_job('win-command')
|
||||
with self.jobLog(job):
|
||||
build = self.history[-1]
|
||||
self.assertEqual(build.result, 'SUCCESS')
|
||||
|
||||
console_output = self.console_output.getvalue()
|
||||
# This should be generic enough to match any callback
|
||||
# plugin failures, which look something like
|
||||
#
|
||||
# [WARNING]: Failure using method (v2_runner_on_ok) in \
|
||||
# callback plugin
|
||||
# (<ansible.plugins.callback.zuul_stream.CallbackModule object at'
|
||||
# 0x7f89f72a20b0>): 'dict' object has no attribute 'startswith'"
|
||||
# Callback Exception:
|
||||
# ...
|
||||
#
|
||||
self.assertNotIn('[WARNING]: Failure using method', console_output)
|
||||
|
||||
text = self._get_job_output(build)
|
||||
data = self._get_job_json(build)
|
||||
|
||||
# The win_ modules do not have a strip_trailing_whitespace
|
||||
# option like the unix ones (and the unix ones default to
|
||||
# true), so we get a CRLF at the end of our stdout stream.
|
||||
token_stdout = "Standard output test {}\r\n".format(
|
||||
self.history[0].jobdir.src_root)
|
||||
# The win_shell module trims the stderr string as part of
|
||||
# its CLIXML handling, so there is no CRLF here.
|
||||
token_stderr = "Standard error test {}".format(
|
||||
self.history[0].jobdir.src_root)
|
||||
result = data[0]['plays'][1]['tasks'][2]['hosts']['compute1']
|
||||
self.assertEqual(token_stdout, result['stdout'])
|
||||
self.assertEqual(token_stderr, result['stderr'])
|
||||
|
||||
# Find the "creates" tasks
|
||||
create1_task = data[0]['plays'][4]['tasks'][3]
|
||||
create1_host = create1_task['hosts']['compute1']
|
||||
self.assertIsNotNone(create1_host['delta'])
|
||||
self.assertNotIn("skipped, since", create1_host.get('msg', ''))
|
||||
self.assertEqual("Creates file that does not exist",
|
||||
create1_task['task']['name'])
|
||||
create2_task = data[0]['plays'][4]['tasks'][4]
|
||||
create2_host = create2_task['hosts']['compute1']
|
||||
self.assertIsNone(create2_host.get('delta'))
|
||||
self.assertIn("skipped, since", create2_host['msg'])
|
||||
self.assertEqual("Creates file that already exists",
|
||||
create2_task['task']['name'])
|
||||
# There is no "delta" returned in this case, so we don't
|
||||
# get a result linee
|
||||
# self.assertLogLine(r'compute1 \| ok: Runtime: None', text)
|
||||
|
||||
self.assertLogLine(
|
||||
r'RUN START: \[untrusted : review.example.com/org/project/'
|
||||
r'playbooks/win-command.yaml@master\]', text)
|
||||
self.assertLogLine(r'PLAY \[all\]', text)
|
||||
self.assertLogLine(
|
||||
r'Ansible version={}'.format(self.ansible_core_version), text)
|
||||
self.assertLogLine(r'TASK \[Show contents of first file\]', text)
|
||||
self.assertLogLine(r'controller \| command test one', text)
|
||||
self.assertLogLine(
|
||||
r'controller \| ok: Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text)
|
||||
self.assertLogLine(r'TASK \[Show contents of second file\]', text)
|
||||
self.assertLogLine(r'compute1 \| command test two', text)
|
||||
self.assertLogLine(r'controller \| command test two', text)
|
||||
self.assertLogLine(r'compute1 \| This is a rescue task', text)
|
||||
self.assertLogLine(r'controller \| This is a rescue task', text)
|
||||
self.assertLogLine(r'compute1 \| This is an always task', text)
|
||||
self.assertLogLine(r'controller \| This is an always task', text)
|
||||
self.assertLogLine(r'compute1 \| This is a handler', text)
|
||||
self.assertLogLine(r'controller \| This is a handler', text)
|
||||
self.assertLogLine(r'controller \| First free task', text)
|
||||
self.assertLogLine(r'controller \| Second free task', text)
|
||||
self.assertLogLine(r'controller \| This is a shell task after an '
|
||||
'included role', text)
|
||||
self.assertLogLine(r'compute1 \| This is a shell task after an '
|
||||
'included role', text)
|
||||
self.assertLogLine(r'controller \| This is a command task after '
|
||||
'an included role', text)
|
||||
self.assertLogLine(r'compute1 \| This is a command task after an '
|
||||
'included role', text)
|
||||
self.assertLogLine(r'controller \| This is a shell task with '
|
||||
'delegate compute1', text)
|
||||
self.assertLogLine(r'controller \| This is a shell task with '
|
||||
'delegate controller', text)
|
||||
self.assertLogLine(r'compute1 \| item_in_loop1', text)
|
||||
self.assertLogLine(r'compute1 \| ok: Item: item_in_loop1 '
|
||||
r'Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text)
|
||||
self.assertLogLine(r'compute1 \| item_in_loop2', text)
|
||||
self.assertLogLine(r'compute1 \| ok: Item: item_in_loop2 '
|
||||
r'Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text)
|
||||
self.assertLogLine(r'compute1 \| failed_in_loop1', text)
|
||||
self.assertLogLine(r'compute1 \| ok: Item: failed_in_loop1 '
|
||||
r'Result: 1', text)
|
||||
self.assertLogLine(r'compute1 \| failed_in_loop2', text)
|
||||
self.assertLogLine(r'compute1 \| ok: Item: failed_in_loop2 '
|
||||
r'Result: 1', text)
|
||||
self.assertLogLine(r'compute1 \| transitive-one', text)
|
||||
self.assertLogLine(r'compute1 \| transitive-two', text)
|
||||
self.assertLogLine(r'compute1 \| transitive-three', text)
|
||||
self.assertLogLine(r'compute1 \| transitive-four', text)
|
||||
self.assertLogLine(
|
||||
r'controller \| ok: Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text)
|
||||
self.assertLogLine('PLAY RECAP', text)
|
||||
self.assertLogLine(
|
||||
r'controller \| ok: \d+ changed: \d+ unreachable: 0 failed: 0 '
|
||||
'skipped: 2 rescued: 1 ignored: 0', text)
|
||||
self.assertLogLine(
|
||||
r'RUN END RESULT_NORMAL: \[untrusted : review.example.com/'
|
||||
r'org/project/playbooks/win-command.yaml@master]', text)
|
||||
time1, time2 = self._getLogTime(r'TASK \[Command Not Found\]',
|
||||
text)
|
||||
self.assertLess((time2 - time1) / timedelta(milliseconds=1),
|
||||
9000)
|
||||
|
||||
# This is from the debug: msg='{{ ansible_version }}'
|
||||
# testing raw variable output. To make it version
|
||||
# agnostic, match just the start of
|
||||
# compute1 | ok: {'string': '2.9.27'...
|
||||
|
||||
# NOTE(ianw) 2022-08-24 : I don't know why the callback
|
||||
# for debug: msg= doesn't put the hostname first like
|
||||
# other output. Undetermined if bug or feature.
|
||||
self.assertLogLineStartsWith(
|
||||
r"""\{'string': '\d.""", text)
|
||||
# ... handling loops is a different path, and that does
|
||||
self.assertLogLineStartsWith(
|
||||
r"""compute1 \| ok: \{'string': '\d.""", text)
|
||||
self.assertLogLine(
|
||||
r'fake \| skipping: Conditional result was False', text)
|
||||
|
||||
|
||||
class TestZuulStream8(AnsibleZuulTestCase, FunctionalZuulStreamMixIn):
|
||||
ansible_version = '8'
|
||||
|
1
zuul/ansible/8/action/win_command.py
Symbolic link
1
zuul/ansible/8/action/win_command.py
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/action/win_command.py
|
1
zuul/ansible/8/action/win_shell.py
Symbolic link
1
zuul/ansible/8/action/win_shell.py
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/action/win_shell.py
|
1
zuul/ansible/8/action/win_zuul_console.py
Symbolic link
1
zuul/ansible/8/action/win_zuul_console.py
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/action/win_zuul_console.py
|
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.ModuleUtils.Zuul.Win.Command.Process.psm1
|
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.ModuleUtils.Zuul.Win.Shell.CommandUtil.psm1
|
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.Zuul.Win.Command.Process.cs
|
1
zuul/ansible/8/library/module_utils/Ansible.Zuul.Win.Common.cs
Symbolic link
1
zuul/ansible/8/library/module_utils/Ansible.Zuul.Win.Common.cs
Symbolic link
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.Zuul.Win.Common.cs
|
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.Zuul.Win.Shell.Process.cs
|
1
zuul/ansible/8/library/module_utils/__init__.py
Symbolic link
1
zuul/ansible/8/library/module_utils/__init__.py
Symbolic link
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/__init__.py
|
1
zuul/ansible/8/library/win_command.ps1
Symbolic link
1
zuul/ansible/8/library/win_command.ps1
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/library/win_command.ps1
|
1
zuul/ansible/8/library/win_shell.ps1
Symbolic link
1
zuul/ansible/8/library/win_shell.ps1
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/library/win_shell.ps1
|
1
zuul/ansible/8/library/win_zuul_console.ps1
Symbolic link
1
zuul/ansible/8/library/win_zuul_console.ps1
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/library/win_zuul_console.ps1
|
1
zuul/ansible/8/win_zuul_console.cs
Symbolic link
1
zuul/ansible/8/win_zuul_console.cs
Symbolic link
@ -0,0 +1 @@
|
||||
../win_zuul_console.cs
|
1
zuul/ansible/9/action/win_command.py
Symbolic link
1
zuul/ansible/9/action/win_command.py
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/action/win_command.py
|
1
zuul/ansible/9/action/win_shell.py
Symbolic link
1
zuul/ansible/9/action/win_shell.py
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/action/win_shell.py
|
1
zuul/ansible/9/action/win_zuul_console.py
Symbolic link
1
zuul/ansible/9/action/win_zuul_console.py
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/action/win_zuul_console.py
|
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.ModuleUtils.Zuul.Win.Command.Process.psm1
|
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.ModuleUtils.Zuul.Win.Shell.CommandUtil.psm1
|
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.Zuul.Win.Command.Process.cs
|
1
zuul/ansible/9/library/module_utils/Ansible.Zuul.Win.Common.cs
Symbolic link
1
zuul/ansible/9/library/module_utils/Ansible.Zuul.Win.Common.cs
Symbolic link
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.Zuul.Win.Common.cs
|
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/Ansible.Zuul.Win.Shell.Process.cs
|
1
zuul/ansible/9/library/module_utils/__init__.py
Symbolic link
1
zuul/ansible/9/library/module_utils/__init__.py
Symbolic link
@ -0,0 +1 @@
|
||||
../../../base/library/module_utils/__init__.py
|
1
zuul/ansible/9/library/win_command.ps1
Symbolic link
1
zuul/ansible/9/library/win_command.ps1
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/library/win_command.ps1
|
1
zuul/ansible/9/library/win_shell.ps1
Symbolic link
1
zuul/ansible/9/library/win_shell.ps1
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/library/win_shell.ps1
|
1
zuul/ansible/9/library/win_zuul_console.ps1
Symbolic link
1
zuul/ansible/9/library/win_zuul_console.ps1
Symbolic link
@ -0,0 +1 @@
|
||||
../../base/library/win_zuul_console.ps1
|
1
zuul/ansible/9/win_zuul_console.cs
Symbolic link
1
zuul/ansible/9/win_zuul_console.cs
Symbolic link
@ -0,0 +1 @@
|
||||
../win_zuul_console.cs
|
55
zuul/ansible/base/action/win_command.py
Normal file
55
zuul/ansible/base/action/win_command.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Copyright 2018 BMW Car IT GmbH
|
||||
# Copyright 2025 Acme Gating, LLC
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from zuul.ansible import paths
|
||||
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
|
||||
normal = paths._import_ansible_action_plugin("normal")
|
||||
|
||||
|
||||
class ActionModule(normal.ActionModule):
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
# Overloading the UUID is a bit lame, but it stops us
|
||||
# having to modify the library command.py too much. Both
|
||||
# of these below stop the creation of the files on disk
|
||||
# for situations where they won't be read and cleaned-up.
|
||||
skip = boolean(
|
||||
self._templar.template(
|
||||
"{{zuul_console_disabled|default(false)}}"))
|
||||
if skip:
|
||||
self._task.args['zuul_log_id'] = 'skip'
|
||||
elif 'ansible_loop_var' in task_vars:
|
||||
# we do not log loops in the zuul_stream.py callback.
|
||||
self._task.args['zuul_log_id'] = 'in-loop-ignore'
|
||||
else:
|
||||
# Get a unique key for ZUUL_LOG_ID_MAP. ZUUL_LOG_ID_MAP
|
||||
# is read-only since we are forked. Use it to add a
|
||||
# counter to the log id so that if we run the same task
|
||||
# more than once, we get a unique log file. See comments
|
||||
# in paths.py for details.
|
||||
log_host = paths._sanitize_filename(
|
||||
task_vars.get('inventory_hostname'))
|
||||
key = "%s-%s" % (self._task._uuid, log_host)
|
||||
count = paths.ZUUL_LOG_ID_MAP.get(key, 0)
|
||||
self._task.args['zuul_log_id'] = "%s-%s-%s" % (
|
||||
self._task._uuid, count, log_host)
|
||||
self._task.args["zuul_output_max_bytes"] = int(
|
||||
os.environ["ZUUL_OUTPUT_MAX_BYTES"])
|
||||
return super(ActionModule, self).run(tmp, task_vars)
|
55
zuul/ansible/base/action/win_shell.py
Normal file
55
zuul/ansible/base/action/win_shell.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Copyright 2018 BMW Car IT GmbH
|
||||
# Copyright 2025 Acme Gating, LLC
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from zuul.ansible import paths
|
||||
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
|
||||
normal = paths._import_ansible_action_plugin("normal")
|
||||
|
||||
|
||||
class ActionModule(normal.ActionModule):
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
# Overloading the UUID is a bit lame, but it stops us
|
||||
# having to modify the library command.py too much. Both
|
||||
# of these below stop the creation of the files on disk
|
||||
# for situations where they won't be read and cleaned-up.
|
||||
skip = boolean(
|
||||
self._templar.template(
|
||||
"{{zuul_console_disabled|default(false)}}"))
|
||||
if skip:
|
||||
self._task.args['zuul_log_id'] = 'skip'
|
||||
elif 'ansible_loop_var' in task_vars:
|
||||
# we do not log loops in the zuul_stream.py callback.
|
||||
self._task.args['zuul_log_id'] = 'in-loop-ignore'
|
||||
else:
|
||||
# Get a unique key for ZUUL_LOG_ID_MAP. ZUUL_LOG_ID_MAP
|
||||
# is read-only since we are forked. Use it to add a
|
||||
# counter to the log id so that if we run the same task
|
||||
# more than once, we get a unique log file. See comments
|
||||
# in paths.py for details.
|
||||
log_host = paths._sanitize_filename(
|
||||
task_vars.get('inventory_hostname'))
|
||||
key = "%s-%s" % (self._task._uuid, log_host)
|
||||
count = paths.ZUUL_LOG_ID_MAP.get(key, 0)
|
||||
self._task.args['zuul_log_id'] = "%s-%s-%s" % (
|
||||
self._task._uuid, count, log_host)
|
||||
self._task.args["zuul_output_max_bytes"] = int(
|
||||
os.environ["ZUUL_OUTPUT_MAX_BYTES"])
|
||||
return super(ActionModule, self).run(tmp, task_vars)
|
53
zuul/ansible/base/action/win_zuul_console.py
Normal file
53
zuul/ansible/base/action/win_zuul_console.py
Normal file
@ -0,0 +1,53 @@
|
||||
# Copyright 2025 Acme Gating, LLC
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import importlib.resources
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.vars import merge_hash
|
||||
|
||||
START_SCRIPT = """
|
||||
(Add-Type -Path "{remote_cs_path}")
|
||||
[WinZuulConsole]::Main($args)
|
||||
"""
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
TRANSFERS_FILES = True
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
results = super(ActionModule, self).run(tmp, task_vars)
|
||||
|
||||
# Copy our csharp server code over
|
||||
local_path = str(importlib.resources.files('zuul').joinpath(
|
||||
'ansible/win_zuul_console.cs'))
|
||||
remote_cs_path = self._connection._shell.join_path(
|
||||
self._connection._shell.tmpdir,
|
||||
'win_zuul_console.cs')
|
||||
self._transfer_file(local_path, remote_cs_path)
|
||||
|
||||
# Copy the bootstrap script
|
||||
remote_ps_path = self._connection._shell.join_path(
|
||||
self._connection._shell.tmpdir,
|
||||
'win_zuul_console_start.ps1')
|
||||
self._transfer_data(
|
||||
remote_ps_path,
|
||||
START_SCRIPT.format(remote_cs_path=remote_cs_path))
|
||||
self._task.args['_zuul_console_exec_path'] = remote_ps_path
|
||||
|
||||
# Run the module
|
||||
results = merge_hash(results, self._execute_module(
|
||||
module_name='win_zuul_console', task_vars=task_vars))
|
||||
return results
|
@ -1,5 +1,5 @@
|
||||
# Copyright 2017 Red Hat, Inc.
|
||||
# Copyright 2024 Acme Gating, LLC
|
||||
# Copyright 2024-2025 Acme Gating, LLC
|
||||
#
|
||||
# Zuul is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -54,10 +54,37 @@ from zuul.ansible import logconfig
|
||||
|
||||
LOG_STREAM_VERSION = 0
|
||||
|
||||
# This is intended to be only used for testing where we change the
|
||||
# port so we can run another instance that doesn't conflict with one
|
||||
# setup by the test environment
|
||||
LOG_STREAM_PORT = int(os.environ.get("ZUUL_CONSOLE_PORT", 19885))
|
||||
DEFAULT_POSIX_PORT = 19885
|
||||
DEFAULT_WINDOWS_PORT = 19886
|
||||
|
||||
POSIX_ACTIONS = (
|
||||
'command',
|
||||
'shell',
|
||||
'ansible.builtin.command',
|
||||
'ansible.builtin.shell',
|
||||
)
|
||||
|
||||
WINDOWS_ACTIONS = (
|
||||
'win_command',
|
||||
'win_shell',
|
||||
'ansible.windows.win_command',
|
||||
'ansible.windows.win_shell',
|
||||
)
|
||||
|
||||
ACTION_LOG_STREAM_PORT = {
|
||||
**{a: DEFAULT_POSIX_PORT for a in POSIX_ACTIONS},
|
||||
**{a: DEFAULT_WINDOWS_PORT for a in WINDOWS_ACTIONS},
|
||||
}
|
||||
|
||||
# Actions that support live log streaming
|
||||
STREAMING_ACTIONS = POSIX_ACTIONS + WINDOWS_ACTIONS
|
||||
|
||||
# Actions that produce stdout/err that should go in the log from JSON
|
||||
OUTPUT_ACTIONS = ('raw',)
|
||||
|
||||
# Actions that produce stdout/err that should go in the log (whether
|
||||
# from streaming or JSON).
|
||||
ALL_ACTIONS = STREAMING_ACTIONS + OUTPUT_ACTIONS
|
||||
|
||||
|
||||
def zuul_filter_result(result):
|
||||
@ -437,13 +464,20 @@ class CallbackModule(default.CallbackModule):
|
||||
if task.async_val:
|
||||
# Don't try to stream from async tasks
|
||||
return
|
||||
if task.action in ('command', 'shell',
|
||||
'ansible.builtin.command', 'ansible.builtin.shell'):
|
||||
if task.action in STREAMING_ACTIONS:
|
||||
play_vars = self._play._variable_manager._hostvars
|
||||
|
||||
hosts = self._get_task_hosts(task)
|
||||
for host, inventory_hostname in hosts:
|
||||
port = LOG_STREAM_PORT
|
||||
default_port = ACTION_LOG_STREAM_PORT.get(task.action)
|
||||
# This is intended to be only used for testing where
|
||||
# we change the port so we can run another instance
|
||||
# that doesn't conflict with one setup by the test
|
||||
# environment
|
||||
port = int(os.environ.get("ZUUL_CONSOLE_PORT", default_port))
|
||||
if port is None:
|
||||
continue
|
||||
|
||||
if (host in ('localhost', '127.0.0.1')):
|
||||
# Don't try to stream from localhost
|
||||
continue
|
||||
@ -462,8 +496,12 @@ class CallbackModule(default.CallbackModule):
|
||||
continue
|
||||
if play_vars[host].get('ansible_connection') in ('kubectl', ):
|
||||
# Stream from the forwarded port on kubectl conns
|
||||
if task.action in WINDOWS_ACTIONS:
|
||||
port_id = 'stream_port2'
|
||||
else:
|
||||
port_id = 'stream_port1'
|
||||
port = play_vars[host]['zuul']['resources'][
|
||||
inventory_hostname].get('stream_port')
|
||||
inventory_hostname].get(port_id)
|
||||
if port is None:
|
||||
self._log("[Zuul] Kubectl and socat must be installed "
|
||||
"on the Zuul executor for streaming output "
|
||||
@ -554,19 +592,11 @@ class CallbackModule(default.CallbackModule):
|
||||
|
||||
if not is_localhost and is_task:
|
||||
self._stop_streamers()
|
||||
if result._task.action in ('raw', 'command', 'shell',
|
||||
'win_command', 'win_shell',
|
||||
'ansible.builtin.raw',
|
||||
'ansible.windows.win_command',
|
||||
'ansible.windows.win_shell'):
|
||||
if result._task.action in ALL_ACTIONS:
|
||||
stdout_lines = zuul_filter_result(result_dict)
|
||||
# We don't have streaming for localhost and windows modules so get
|
||||
# standard out after the fact.
|
||||
if is_localhost or result._task.action in (
|
||||
'raw', 'win_command', 'win_shell',
|
||||
'ansible.builtin.raw',
|
||||
'ansible.windows.win_command',
|
||||
'ansible.windows.win_shell'):
|
||||
# We don't have streaming for localhost so get standard
|
||||
# out after the fact.
|
||||
if is_localhost or result._task.action in OUTPUT_ACTIONS:
|
||||
for line in stdout_lines:
|
||||
hostname = self._get_hostname(result)
|
||||
self._log("%s | %s " % (hostname, line))
|
||||
@ -645,6 +675,17 @@ class CallbackModule(default.CallbackModule):
|
||||
if 'not run command since' in result_dict.get('msg', ''):
|
||||
self._stop_skipped_task_streamer(result._task)
|
||||
|
||||
if result._task.action in ('win_command', 'win_shell'):
|
||||
# The win_command module has a small set of msgs it returns;
|
||||
# we can use that to detect if decided not to execute the
|
||||
# command:
|
||||
# "skipped, since $creates exists" and "skipped, since
|
||||
# $removes does not exist" are the messages we're looking
|
||||
# for.
|
||||
m = result_dict.get('msg', '')
|
||||
if 'skipped, since' in m and 'exist' in m:
|
||||
self._stop_skipped_task_streamer(result._task)
|
||||
|
||||
if (self._play.strategy == 'free'
|
||||
and self._last_task_banner != result._task._uuid):
|
||||
self._print_task_banner(result._task)
|
||||
@ -705,7 +746,7 @@ class CallbackModule(default.CallbackModule):
|
||||
self._log_message(
|
||||
msg=json.dumps(result_dict, indent=2, sort_keys=True),
|
||||
status=status, result=result)
|
||||
elif result._task.action not in ('command', 'shell'):
|
||||
elif result._task.action not in STREAMING_ACTIONS:
|
||||
if 'msg' in result_dict:
|
||||
self._log_message(msg=result_dict['msg'],
|
||||
result=result, status=status)
|
||||
@ -750,11 +791,7 @@ class CallbackModule(default.CallbackModule):
|
||||
|
||||
if to_text(result_dict.get('msg', '')).startswith('MODULE FAILURE'):
|
||||
self._log_module_failure(result, result_dict)
|
||||
elif result._task.action not in ('raw', 'command', 'shell',
|
||||
'win_command', 'win_shell',
|
||||
'ansible.builtin.raw',
|
||||
'ansible.windows.win_command',
|
||||
'ansible.windows.win_shell'):
|
||||
elif result._task.action not in ALL_ACTIONS:
|
||||
if 'msg' in result_dict:
|
||||
self._log_message(
|
||||
result=result, msg=result_dict['msg'], status=status)
|
||||
@ -796,11 +833,7 @@ class CallbackModule(default.CallbackModule):
|
||||
|
||||
if to_text(result_dict.get('msg', '')).startswith('MODULE FAILURE'):
|
||||
self._log_module_failure(result, result_dict)
|
||||
elif result._task.action not in ('raw', 'command', 'shell',
|
||||
'win_command', 'win_shell',
|
||||
'ansible.builtin.raw',
|
||||
'ansible.windows.win_command',
|
||||
'ansible.windows.win_shell'):
|
||||
elif result._task.action not in ALL_ACTIONS:
|
||||
self._log_message(
|
||||
result=result,
|
||||
msg="Item: {loop_var}".format(loop_var=result_dict[loop_var]),
|
||||
@ -879,9 +912,10 @@ class CallbackModule(default.CallbackModule):
|
||||
is_shell = task_args.pop('_uses_shell', False)
|
||||
if is_shell and task_name == 'command':
|
||||
task_name = 'shell'
|
||||
# win_shell doesn't use _uses_shell.
|
||||
raw_params = task_args.pop('_raw_params', '').split('\n')
|
||||
# If there's just a single line, go ahead and print it
|
||||
if len(raw_params) == 1 and task_name in ('shell', 'command'):
|
||||
if len(raw_params) == 1 and task_name in STREAMING_ACTIONS:
|
||||
task_name = '{name}: {command}'.format(
|
||||
name=task_name, command=raw_params[0])
|
||||
|
||||
|
@ -0,0 +1,255 @@
|
||||
# This is based on https://github.com/ansible-collections/ansible.windows/blob/2.5.0/plugins/module_utils/Process.psm1
|
||||
# Copyright (c) 2020 Ansible Project
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Zuul.Win.Common
|
||||
#AnsibleRequires -CSharpUtil Ansible.Zuul.Win.Command.Process
|
||||
|
||||
Function Resolve-ExecutablePath {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tries to resolve the file path to a valid executable.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
$FilePath,
|
||||
|
||||
[String]
|
||||
$WorkingDirectory
|
||||
)
|
||||
|
||||
# Ensure the path has an extension set, default to .exe
|
||||
if (-not [IO.Path]::HasExtension($FilePath)) {
|
||||
$FilePath = "$FilePath.exe"
|
||||
}
|
||||
|
||||
# See the if path is resolvable using the normal PATH logic. Also resolves absolute paths and relative paths if
|
||||
# they exist.
|
||||
$command = @(Get-Command -Name $FilePath -CommandType Application -ErrorAction SilentlyContinue)
|
||||
if ($command) {
|
||||
$command[0].Path
|
||||
return
|
||||
}
|
||||
|
||||
# If -WorkingDirectory is specified, check if the path is relative to that
|
||||
if ($WorkingDirectory) {
|
||||
$file = $PSCmdlet.GetUnresolvedProviderPathFromPSPath((Join-Path -Path $WorkingDirectory -ChildPath $FilePath))
|
||||
if (Test-Path -LiteralPath $file) {
|
||||
$file
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Just hope for the best and use whatever was provided.
|
||||
$FilePath
|
||||
}
|
||||
|
||||
Function ConvertFrom-EscapedArgument {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extract individual arguments from a command line string.
|
||||
|
||||
.PARAMETER InputObject
|
||||
The command line string to extract the arguments from.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
[OutputType([String])]
|
||||
param (
|
||||
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
|
||||
[String[]]
|
||||
$InputObject
|
||||
)
|
||||
|
||||
process {
|
||||
foreach ($command in $InputObject) {
|
||||
# CommandLineToArgv treats \" slightly different for the first argument for some reason (probably because
|
||||
# it expects it to be a filepath). We add a dummy value to ensure it splits the args in the same way as
|
||||
# each other and just discard that first arg in the output.
|
||||
$command = "a $command"
|
||||
[Ansible.Zuul.Win.Command.Process.ProcessUtil]::CommandLineToArgv($command) | Select-Object -Skip 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Function ConvertTo-EscapedArgument {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Escapes an argument value so it can be used in a call to CreateProcess.
|
||||
|
||||
.PARAMETER InputObject
|
||||
The argument(s) to escape.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
[OutputType([String])]
|
||||
param (
|
||||
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
|
||||
[AllowEmptyString()]
|
||||
[AllowNull()]
|
||||
[String[]]
|
||||
$InputObject
|
||||
)
|
||||
|
||||
process {
|
||||
if (-not $InputObject) {
|
||||
return '""'
|
||||
}
|
||||
|
||||
foreach ($argument in $InputObject) {
|
||||
if (-not $argument) {
|
||||
return '""'
|
||||
}
|
||||
elseif ($argument -notmatch '[\s"]') {
|
||||
return $argument
|
||||
}
|
||||
|
||||
# Replace any double quotes in an argument with '\"'
|
||||
$argument = $argument -replace '"', '\"'
|
||||
|
||||
# Double up on any '\' chars that preceded '\"'
|
||||
$argument = $argument -replace '(\\+)\\"', '$1$1\"'
|
||||
|
||||
# Double up '\' at the end of the argument so it doesn't escape end quote.
|
||||
$argument = $argument -replace '(\\+)$', '$1$1'
|
||||
|
||||
# Finally wrap the entire argument in double quotes now we've escaped the double quotes within
|
||||
'"{0}"' -f $argument
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Function Start-AnsibleWindowsProcess {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Start a process and wait for it to finish.
|
||||
|
||||
.PARAMETER FilePath
|
||||
The file to execute.
|
||||
|
||||
.PARAMETER ArgumentList
|
||||
Arguments to execute, these will be escaped so the literal value is used.
|
||||
|
||||
.PARAMETER CommandLine
|
||||
The raw command line to call with CreateProcess. These values are not escaped for you so use at your own risk.
|
||||
|
||||
.PARAMETER WorkingDirectory
|
||||
The working directory to set on the new process, defaults to the current working dir.
|
||||
|
||||
.PARAMETER Environment
|
||||
Override the environment to set for the new process, if not set then the current environment will be used.
|
||||
|
||||
.PARAMETER InputObject
|
||||
A string or byte[] array to send to the process' stdin when it has started.
|
||||
|
||||
.PARAMETER OutputEncodingOverride
|
||||
The encoding name to use when reading the stdout/stderr of the process. Defaults to utf-8 if not set.
|
||||
|
||||
.PARAMETER WaitChildren
|
||||
Whether to wait for any child process spawned to finish before returning. This only works on Windows hosts on
|
||||
Server 2012/Windows 8 or newer.
|
||||
|
||||
.OUTPUTS
|
||||
[PSCustomObject]@{
|
||||
Command = The final command used to start the process
|
||||
Stdout = The stdout of the process
|
||||
Stderr = The stderr of the process
|
||||
ExitCode = The return code from the process
|
||||
}
|
||||
#>
|
||||
[CmdletBinding(DefaultParameterSetName = 'ArgumentList')]
|
||||
[OutputType('Ansible.Zuul.Win.Command.Process.Info')]
|
||||
param (
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'ArgumentList')]
|
||||
[Parameter(ParameterSetName = 'CommandLine')]
|
||||
[String]
|
||||
$FilePath,
|
||||
|
||||
[Parameter(ParameterSetName = 'ArgumentList')]
|
||||
[String[]]
|
||||
$ArgumentList,
|
||||
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'CommandLine')]
|
||||
[String]
|
||||
$CommandLine,
|
||||
|
||||
[String]
|
||||
# Default to the PowerShell location and not the process location.
|
||||
$WorkingDirectory = (Get-Location -PSProvider FileSystem),
|
||||
|
||||
[Collections.IDictionary]
|
||||
$Environment,
|
||||
|
||||
[Object]
|
||||
$InputObject,
|
||||
|
||||
[String]
|
||||
[Alias('OutputEncoding')]
|
||||
$OutputEncodingOverride,
|
||||
|
||||
[Switch]
|
||||
$WaitChildren,
|
||||
|
||||
[String]
|
||||
$ZuulLogId,
|
||||
|
||||
[String]
|
||||
$ZuulLogPath,
|
||||
|
||||
[UInt32]
|
||||
$ZuulOutputMaxBytes
|
||||
)
|
||||
|
||||
if ($WorkingDirectory) {
|
||||
if (-not (Test-Path -LiteralPath $WorkingDirectory)) {
|
||||
Write-Error -Message "Could not find specified -WorkingDirectory '$WorkingDirectory'"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ($FilePath) {
|
||||
$applicationName = $FilePath
|
||||
}
|
||||
else {
|
||||
# If -FilePath is not set then -CommandLine must have been used. Select the path based on the first entry.
|
||||
$applicationName = [Ansible.Zuul.Win.Command.Process.ProcessUtil]::CommandLineToArgv($CommandLine)[0]
|
||||
}
|
||||
$applicationName = Resolve-ExecutablePath -FilePath $applicationName -WorkingDirectory $WorkingDirectory
|
||||
|
||||
# When -ArgumentList is used, we need to escape each argument, including the FilePath to build our CommandLine.
|
||||
if ($PSCmdlet.ParameterSetName -eq 'ArgumentList') {
|
||||
$CommandLine = ConvertTo-EscapedArgument -InputObject $applicationName
|
||||
if ($ArgumentList.Count) {
|
||||
$escapedArguments = @($ArgumentList | ConvertTo-EscapedArgument)
|
||||
$CommandLine += " $($escapedArguments -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
$stdin = $null
|
||||
if ($InputObject) {
|
||||
if ($InputObject -is [byte[]]) {
|
||||
$stdin = $InputObject
|
||||
}
|
||||
elseif ($InputObject -is [string]) {
|
||||
$stdin = [Text.Encoding]::UTF8.GetBytes($InputObject)
|
||||
}
|
||||
else {
|
||||
Write-Error -Message "InputObject must be a string or byte[]"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$res = [Ansible.Zuul.Win.Command.Process.ProcessUtil]::CreateProcess($applicationName, $CommandLine, $WorkingDirectory, $Environment, $stdin, $OutputEncodingOverride, $WaitChildren, $ZuulLogId, $ZuulLogPath, $ZuulOutputMaxBytes)
|
||||
|
||||
[PSCustomObject]@{
|
||||
PSTypeName = 'Ansible.Zuul.Win.Command.Process.Info'
|
||||
Command = $CommandLine
|
||||
Stdout = $res.StandardOut
|
||||
Stderr = $res.StandardError
|
||||
ExitCode = $res.ExitCode
|
||||
}
|
||||
}
|
||||
|
||||
$exportMembers = @{
|
||||
Function = 'ConvertFrom-EscapedArgument', 'ConvertTo-EscapedArgument', 'Resolve-ExecutablePath', 'Start-AnsibleWindowsProcess'
|
||||
}
|
||||
Export-ModuleMember @exportMembers
|
@ -0,0 +1,112 @@
|
||||
# This is based on: https://github.com/ansible/ansible/blob/v2.16.14/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Zuul.Win.Common
|
||||
#AnsibleRequires -CSharpUtil Ansible.Zuul.Win.Shell.Process
|
||||
|
||||
Function Get-ExecutablePath {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get's the full path to an executable, will search the directory specified or ones in the PATH env var.
|
||||
|
||||
.PARAMETER executable
|
||||
[String]The executable to search for.
|
||||
|
||||
.PARAMETER directory
|
||||
[String] If set, the directory to search in.
|
||||
|
||||
.OUTPUT
|
||||
[String] The full path the executable specified.
|
||||
#>
|
||||
Param(
|
||||
[String]$executable,
|
||||
[String]$directory = $null
|
||||
)
|
||||
|
||||
# we need to add .exe if it doesn't have an extension already
|
||||
if (-not [System.IO.Path]::HasExtension($executable)) {
|
||||
$executable = "$($executable).exe"
|
||||
}
|
||||
$full_path = [System.IO.Path]::GetFullPath($executable)
|
||||
|
||||
if ($full_path -ne $executable -and $directory -ne $null) {
|
||||
$file = Get-Item -LiteralPath "$directory\$executable" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
else {
|
||||
$file = Get-Item -LiteralPath $executable -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if ($null -ne $file) {
|
||||
$executable_path = $file.FullName
|
||||
}
|
||||
else {
|
||||
$executable_path = [Ansible.Zuul.Win.Shell.Process.ProcessUtil]::SearchPath($executable)
|
||||
}
|
||||
return $executable_path
|
||||
}
|
||||
|
||||
Function Run-Command {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run a command with the CreateProcess API and return the stdout/stderr and return code.
|
||||
|
||||
.PARAMETER command
|
||||
The full command, including the executable, to run.
|
||||
|
||||
.PARAMETER working_directory
|
||||
The working directory to set on the new process, will default to the current working dir.
|
||||
|
||||
.PARAMETER stdin
|
||||
A string to sent over the stdin pipe to the new process.
|
||||
|
||||
.PARAMETER environment
|
||||
A hashtable of key/value pairs to run with the command. If set, it will replace all other env vars.
|
||||
|
||||
.PARAMETER output_encoding_override
|
||||
The character encoding name for decoding stdout/stderr output of the process.
|
||||
|
||||
.OUTPUT
|
||||
[Hashtable]
|
||||
[String]executable - The full path to the executable that was run
|
||||
[String]stdout - The stdout stream of the process
|
||||
[String]stderr - The stderr stream of the process
|
||||
[Int32]rc - The return code of the process
|
||||
#>
|
||||
Param(
|
||||
[string]$command,
|
||||
[string]$working_directory = $null,
|
||||
[string]$stdin = "",
|
||||
[hashtable]$environment = @{},
|
||||
[string]$output_encoding_override = $null,
|
||||
[String]$zuul_log_id,
|
||||
[String]$zuul_log_path,
|
||||
[UInt32]$zuul_output_max_bytes
|
||||
)
|
||||
|
||||
# need to validate the working directory if it is set
|
||||
if ($working_directory) {
|
||||
# validate working directory is a valid path
|
||||
if (-not (Test-Path -LiteralPath $working_directory)) {
|
||||
throw "invalid working directory path '$working_directory'"
|
||||
}
|
||||
}
|
||||
|
||||
# lpApplicationName needs to be the full path to an executable, we do this
|
||||
# by getting the executable as the first arg and then getting the full path
|
||||
$arguments = [Ansible.Zuul.Win.Shell.Process.ProcessUtil]::ParseCommandLine($command)
|
||||
$executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory
|
||||
|
||||
# run the command and get the results
|
||||
$command_result = [Ansible.Zuul.Win.Shell.Process.ProcessUtil]::CreateProcess($executable, $command, $working_directory, $environment, $stdin, $output_encoding_override, $zuul_log_id, $zuul_log_path, $zuul_output_max_bytes)
|
||||
|
||||
return , @{
|
||||
executable = $executable
|
||||
stdout = $command_result.StandardOut
|
||||
stderr = $command_result.StandardError
|
||||
rc = $command_result.ExitCode
|
||||
}
|
||||
}
|
||||
|
||||
# this line must stay at the bottom to ensure all defined module parts are exported
|
||||
Export-ModuleMember -Function Get-ExecutablePath, Run-Command
|
@ -0,0 +1,997 @@
|
||||
// This is based on https://github.com/ansible-collections/ansible.windows/blob/2.5.0/plugins/module_utils/Process.cs
|
||||
// That file does not have a license header, so it is presumed to use
|
||||
// the GPLv3 as noted in the root of the repo.
|
||||
// GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
// Zuul note: This file, originating in the ansible.windows
|
||||
// collection, is similar to a file in ansible-core, but this file is
|
||||
// only used by the win_command module while the win_shell module uses
|
||||
// the version in core. Therefore, in Zuul, this file and namespace
|
||||
// have adopted the name "Ansible.Zuul.Win.Command.Process" to
|
||||
// delineate that this is the Zuul version of the Process module used
|
||||
// by win_command.
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
using Ansible.Zuul.Win.Common;
|
||||
|
||||
//TypeAccelerator -Name Ansible.Zuul.Win.Command.Process.ProcessInformation -TypeName ProcessInformation
|
||||
//TypeAccelerator -Name Ansible.Zuul.Win.Command.Process.ProcessUtil -TypeName ProcessUtil
|
||||
//TypeAccelerator -Name Ansible.Zuul.Win.Command.Process.Result -TypeName Result
|
||||
//TypeAccelerator -Name Ansible.Zuul.Win.Command.Process.SecurityAttributes -TypeName SecurityAttributes
|
||||
//TypeAccelerator -Name Ansible.Zuul.Win.Command.Process.StartupInfo -TypeName StartupInfo
|
||||
|
||||
namespace Ansible.Zuul.Win.Command.Process
|
||||
{
|
||||
internal class NativeHelpers
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct JOBOBJECT_ASSOCIATE_COMPLETION_PORT
|
||||
{
|
||||
public IntPtr CompletionKey;
|
||||
public IntPtr CompletionPort;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class SECURITY_ATTRIBUTES
|
||||
{
|
||||
public UInt32 nLength;
|
||||
public IntPtr lpSecurityDescriptor;
|
||||
public bool bInheritHandle = false;
|
||||
public SECURITY_ATTRIBUTES()
|
||||
{
|
||||
nLength = (UInt32)Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class STARTUPINFOW
|
||||
{
|
||||
public UInt32 cb;
|
||||
public IntPtr lpReserved;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string lpDesktop;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string lpTitle;
|
||||
public UInt32 dwX;
|
||||
public UInt32 dwY;
|
||||
public UInt32 dwXSize;
|
||||
public UInt32 dwYSize;
|
||||
public UInt32 dwXCountChars;
|
||||
public UInt32 dwYCountChars;
|
||||
public UInt32 dwFillAttribute;
|
||||
public StartupInfoFlags dwFlags;
|
||||
public UInt16 wShowWindow;
|
||||
public UInt16 cbReserved2;
|
||||
public IntPtr lpReserved2;
|
||||
public SafeHandle hStdInput = new SafeNativeHandle(IntPtr.Zero, false);
|
||||
public SafeHandle hStdOutput = new SafeNativeHandle(IntPtr.Zero, false);
|
||||
public SafeHandle hStdError = new SafeNativeHandle(IntPtr.Zero, false);
|
||||
|
||||
public STARTUPINFOW()
|
||||
{
|
||||
cb = (UInt32)Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class STARTUPINFOEX
|
||||
{
|
||||
public STARTUPINFOW startupInfo;
|
||||
public SafeHandle lpAttributeList = new SafeNativeHandle(IntPtr.Zero, false);
|
||||
public STARTUPINFOEX()
|
||||
{
|
||||
startupInfo = new STARTUPINFOW();
|
||||
startupInfo.cb = (UInt32)Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct PROCESS_INFORMATION
|
||||
{
|
||||
public IntPtr hProcess;
|
||||
public IntPtr hThread;
|
||||
public int dwProcessId;
|
||||
public int dwThreadId;
|
||||
}
|
||||
|
||||
|
||||
[Flags]
|
||||
public enum DuplicateHandleOptions : uint
|
||||
{
|
||||
NONE = 0x0000000,
|
||||
DUPLICATE_CLOSE_SOURCE = 0x00000001,
|
||||
DUPLICATE_SAME_ACCESS = 0x00000002,
|
||||
}
|
||||
|
||||
public enum JobObjectInformationClass : uint
|
||||
{
|
||||
JobObjectAssociateCompletionPortInformation = 7,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum StartupInfoFlags : uint
|
||||
{
|
||||
STARTF_USESHOWWINDOW = 0x00000001,
|
||||
USESTDHANDLES = 0x00000100,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum HandleFlags : uint
|
||||
{
|
||||
None = 0,
|
||||
INHERIT = 1
|
||||
}
|
||||
}
|
||||
|
||||
internal class NativeMethods
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool AssignProcessToJobObject(
|
||||
SafeHandle hJob,
|
||||
IntPtr hProcess);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool CloseHandle(
|
||||
IntPtr hObject);
|
||||
|
||||
[DllImport("shell32.dll", SetLastError = true)]
|
||||
public static extern SafeMemoryBuffer CommandLineToArgvW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
|
||||
out int pNumArgs);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern SafeNativeHandle CreateIoCompletionPort(
|
||||
IntPtr FileHandle,
|
||||
IntPtr ExistingCompletionPort,
|
||||
UIntPtr CompletionKey,
|
||||
UInt32 NumberOfConcurrentThreads);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern SafeNativeHandle CreateJobObjectW(
|
||||
IntPtr lpJobAttributes,
|
||||
string lpName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool CreatePipe(
|
||||
out SafeFileHandle hReadPipe,
|
||||
out SafeFileHandle hWritePipe,
|
||||
NativeHelpers.SECURITY_ATTRIBUTES lpPipeAttributes,
|
||||
UInt32 nSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern bool CreateProcessW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName,
|
||||
StringBuilder lpCommandLine,
|
||||
SafeMemoryBuffer lpProcessAttributes,
|
||||
SafeMemoryBuffer lpThreadAttributes,
|
||||
bool bInheritHandles,
|
||||
ProcessCreationFlags dwCreationFlags,
|
||||
SafeMemoryBuffer lpEnvironment,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory,
|
||||
NativeHelpers.STARTUPINFOEX lpStartupInfo,
|
||||
out NativeHelpers.PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern bool TerminateProcess(
|
||||
SafeHandle hProcess,
|
||||
UInt32 lpExitCode);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern void DeleteProcThreadAttributeList(
|
||||
IntPtr lpAttributeList);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool DuplicateHandle(
|
||||
SafeHandle hSourceProcessHandle,
|
||||
SafeHandle hSourceHandle,
|
||||
SafeHandle hTargetProcessHandle,
|
||||
out IntPtr lpTargetHandle,
|
||||
UInt32 dwDesiredAccess,
|
||||
bool bInheritHandle,
|
||||
NativeHelpers.DuplicateHandleOptions dwOptions);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool FreeConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetConsoleWindow();
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern IntPtr GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool GetExitCodeProcess(
|
||||
SafeHandle hProcess,
|
||||
out UInt32 lpExitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool GetQueuedCompletionStatus(
|
||||
SafeHandle CompletionPort,
|
||||
out UInt32 lpNumberOfBytesTransferred,
|
||||
out UIntPtr lpCompletionKey,
|
||||
out IntPtr lpOverlapped,
|
||||
UInt32 dwMilliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool InitializeProcThreadAttributeList(
|
||||
IntPtr lpAttributeList,
|
||||
Int32 dwAttributeCount,
|
||||
UInt32 dwFlags,
|
||||
ref IntPtr lpSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern SafeNativeHandle OpenProcess(
|
||||
Int32 dwDesiredAccess,
|
||||
bool bInheritHandle,
|
||||
Int32 dwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern UInt32 ResumeThread(
|
||||
SafeHandle hThread);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetConsoleCP(
|
||||
UInt32 wCodePageID);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetConsoleOutputCP(
|
||||
UInt32 wCodePageID);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetHandleInformation(
|
||||
SafeHandle hObject,
|
||||
NativeHelpers.HandleFlags dwMask,
|
||||
NativeHelpers.HandleFlags dwFlags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetInformationJobObject(
|
||||
SafeHandle hJob,
|
||||
NativeHelpers.JobObjectInformationClass JobObjectInformationClass,
|
||||
IntPtr lpJobObjectInformation,
|
||||
Int32 cbJobObjectInformationLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool UpdateProcThreadAttribute(
|
||||
SafeHandle lpAttributeList,
|
||||
UInt32 dwFlags,
|
||||
UIntPtr Attribute,
|
||||
SafeHandle lpValue,
|
||||
UIntPtr cbSize,
|
||||
IntPtr lpPreviousValue,
|
||||
IntPtr lpReturnSize);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern UInt32 WaitForSingleObject(
|
||||
SafeHandle hHandle,
|
||||
UInt32 dwMilliseconds);
|
||||
}
|
||||
|
||||
internal class SafeDuplicateHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
private readonly SafeHandle _process;
|
||||
private readonly bool _ownsHandle;
|
||||
|
||||
public SafeDuplicateHandle(IntPtr handle, SafeHandle process) : this(handle, process, true) { }
|
||||
|
||||
public SafeDuplicateHandle(IntPtr handle, SafeHandle process, bool ownsHandle) : base(true)
|
||||
{
|
||||
SetHandle(handle);
|
||||
_process = process;
|
||||
_ownsHandle = ownsHandle;
|
||||
}
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
if (_ownsHandle)
|
||||
{
|
||||
// Cannot pass this SafeHandle object to DuplicateHandle as it
|
||||
// will appeared as closed/invalid already. Use a temporary
|
||||
// SafeHandle that is set not to dispose itself.
|
||||
ProcessUtil.DuplicateHandle(
|
||||
_process,
|
||||
new SafeNativeHandle(handle, false),
|
||||
null,
|
||||
0,
|
||||
false,
|
||||
NativeHelpers.DuplicateHandleOptions.DUPLICATE_CLOSE_SOURCE,
|
||||
false);
|
||||
_process.Dispose();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
public SafeMemoryBuffer() : base(true) { }
|
||||
public SafeMemoryBuffer(int cb) : base(true)
|
||||
{
|
||||
base.SetHandle(Marshal.AllocHGlobal(cb));
|
||||
}
|
||||
public SafeMemoryBuffer(IntPtr handle) : base(true)
|
||||
{
|
||||
base.SetHandle(handle);
|
||||
}
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
Marshal.FreeHGlobal(handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal class SafeProcThreadAttribute : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
internal List<SafeHandle> values = new List<SafeHandle>();
|
||||
|
||||
public SafeProcThreadAttribute() : base(true) { }
|
||||
public SafeProcThreadAttribute(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHandle)
|
||||
{
|
||||
SetHandle(preexistingHandle);
|
||||
}
|
||||
|
||||
public void AddValue(SafeHandle value)
|
||||
{
|
||||
values.Add(value);
|
||||
}
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
foreach (SafeHandle val in values)
|
||||
{
|
||||
val.Dispose();
|
||||
}
|
||||
|
||||
NativeMethods.DeleteProcThreadAttributeList(handle);
|
||||
Marshal.FreeHGlobal(handle);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ProcessCreationFlags : uint
|
||||
{
|
||||
None = 0x00000000,
|
||||
DebugProcess = 0x00000001,
|
||||
DebugOnlyThisProcess = 0x00000002,
|
||||
CreateSuspended = 0x00000004,
|
||||
DetachedProcess = 0x00000008,
|
||||
CreateNewConsole = 0x00000010,
|
||||
NormalPriorityClass = 0x00000020,
|
||||
IdlePriorityClass = 0x00000040,
|
||||
HighPriorityClass = 0x00000080,
|
||||
RealtimePriorityClass = 0x00000100,
|
||||
CreateNewProcessGroup = 0x00000200,
|
||||
CreateUnicodeEnvironment = 0x00000400,
|
||||
CreateSeparateWowVdm = 0x00000800,
|
||||
CreateSharedWowVdm = 0x00001000,
|
||||
CreateForceDos = 0x00002000,
|
||||
BelowNormalPriorityClass = 0x00004000,
|
||||
AboveNormalPriorityClass = 0x00008000,
|
||||
InheritParentAffinity = 0x00010000,
|
||||
InheritCallerPriority = 0x00020000,
|
||||
CreateProtectedProcess = 0x00040000,
|
||||
ExtendedStartupInfoPresent = 0x00080000,
|
||||
ProcessModeBackgroundBegin = 0x00100000,
|
||||
ProcessModeBackgroundEnd = 0x00200000,
|
||||
CreateSecureProcess = 0x00400000,
|
||||
CreateBreakawayFromJob = 0x01000000,
|
||||
CreatePreserveCodeAuthzLevel = 0x02000000,
|
||||
CreateDefaultErrorMode = 0x04000000,
|
||||
CreateNoWindow = 0x08000000,
|
||||
ProfileUser = 0x10000000,
|
||||
ProfileKernel = 0x20000000,
|
||||
ProfileServer = 0x40000000,
|
||||
CreateIgnoreSystemDefault = 0x80000000,
|
||||
}
|
||||
|
||||
public class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
public SafeNativeHandle() : base(true) { }
|
||||
public SafeNativeHandle(IntPtr handle) : this(handle, true) { }
|
||||
public SafeNativeHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) { this.handle = handle; }
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
return NativeMethods.CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
public class Win32Exception : System.ComponentModel.Win32Exception
|
||||
{
|
||||
private string _msg;
|
||||
|
||||
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
||||
public Win32Exception(int errorCode, string message) : base(errorCode)
|
||||
{
|
||||
_msg = String.Format("{0} ({1}, Win32ErrorCode {2} - 0x{2:X8})", message, base.Message, errorCode);
|
||||
}
|
||||
|
||||
public override string Message { get { return _msg; } }
|
||||
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
|
||||
}
|
||||
|
||||
public class Result
|
||||
{
|
||||
public string StandardOut { get; internal set; }
|
||||
public string StandardError { get; internal set; }
|
||||
public uint ExitCode { get; internal set; }
|
||||
}
|
||||
|
||||
public class ProcessInformation : IDisposable
|
||||
{
|
||||
public SafeNativeHandle Process { get; internal set; }
|
||||
public SafeNativeHandle Thread { get; internal set; }
|
||||
public int ProcessId { get; internal set; }
|
||||
public int ThreadId { get; internal set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Process != null)
|
||||
Process.Dispose();
|
||||
|
||||
if (Thread != null)
|
||||
Thread.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
~ProcessInformation() { Dispose(); }
|
||||
}
|
||||
|
||||
public class SecurityAttributes
|
||||
{
|
||||
public bool InheritHandle { get; set; }
|
||||
// TODO: Support SecurityDescriptor at some point.
|
||||
// Should it use RawSecurityDescriptor or create a Process SD class that inherits NativeObjectSecurity?
|
||||
}
|
||||
|
||||
public class StartupInfo
|
||||
{
|
||||
public string Desktop { get; set; }
|
||||
public string Title { get; set; }
|
||||
public ProcessWindowStyle? WindowStyle { get; set; }
|
||||
public SafeHandle StandardInput { get; set; }
|
||||
public SafeHandle StandardOutput { get; set; }
|
||||
public SafeHandle StandardError { get; set; }
|
||||
public int ParentProcess { get; set; }
|
||||
|
||||
// TODO: Support PROC_THREAD_ATTRIBUTE_HANDLE_LIST
|
||||
}
|
||||
|
||||
public class ProcessUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a command line string into an argv array according to the Windows rules
|
||||
/// </summary>
|
||||
/// <param name="lpCommandLine">The command line to parse</param>
|
||||
/// <returns>An array of arguments interpreted by Windows</returns>
|
||||
public static string[] CommandLineToArgv(string lpCommandLine)
|
||||
{
|
||||
int numArgs;
|
||||
using (SafeMemoryBuffer buf = NativeMethods.CommandLineToArgvW(lpCommandLine, out numArgs))
|
||||
{
|
||||
if (buf.IsInvalid)
|
||||
throw new Win32Exception("Error parsing command line");
|
||||
IntPtr[] strptrs = new IntPtr[numArgs];
|
||||
Marshal.Copy(buf.DangerousGetHandle(), strptrs, 0, numArgs);
|
||||
return strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a process based on the CreateProcess API call and wait for it to complete.
|
||||
/// </summary>
|
||||
/// <param name="lpApplicationName">The name of the executable or batch file to execute</param>
|
||||
/// <param name="lpCommandLine">The command line to execute, typically this includes lpApplication as the first argument</param>
|
||||
/// <param name="lpCurrentDirectory">The full path to the current directory for the process, null will have the same cwd as the calling process</param>
|
||||
/// <param name="environment">A dictionary of key/value pairs to define the new process environment</param>
|
||||
/// <param name="stdin">A byte array to send over the stdin pipe</param>
|
||||
/// <param name="outputEncoding">The character encoding for decoding stdout/stderr output of the process.</param>
|
||||
/// <param name="waitChildren">Whether to wait for any children spawned by the process to finished (Server2012 +).</param>
|
||||
/// <returns>Result object that contains the command output and return code</returns>
|
||||
public static Result CreateProcess(
|
||||
string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||
IDictionary environment, byte[] stdin, string outputEncoding,
|
||||
bool waitChildren, string zuulLogId, string zuulLogPath, UInt32 zuulOutputMaxBytes)
|
||||
{
|
||||
ProcessCreationFlags creationFlags = ProcessCreationFlags.CreateSuspended |
|
||||
ProcessCreationFlags.CreateUnicodeEnvironment;
|
||||
StartupInfo si = new StartupInfo();
|
||||
ProcessInformation pi = null;
|
||||
|
||||
SafeFileHandle stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinRead, stdinWrite;
|
||||
CreateStdioPipes(si, out stdoutRead, out stdoutWrite, out stderrRead, out stderrWrite, out stdinRead,
|
||||
out stdinWrite);
|
||||
|
||||
using (stdoutRead)
|
||||
using (stdoutWrite)
|
||||
using (stderrRead)
|
||||
using (stderrWrite)
|
||||
using (stdinRead)
|
||||
using (stdinWrite)
|
||||
{
|
||||
FileStream stdinStream = new FileStream(stdinWrite, FileAccess.Write);
|
||||
|
||||
bool isConsole = false;
|
||||
if (NativeMethods.GetConsoleWindow() == IntPtr.Zero)
|
||||
{
|
||||
isConsole = NativeMethods.AllocConsole();
|
||||
|
||||
// Set console input/output codepage to UTF-8
|
||||
NativeMethods.SetConsoleCP(65001);
|
||||
NativeMethods.SetConsoleOutputCP(65001);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
pi = NativeCreateProcess(lpApplicationName, lpCommandLine, null, null, true, creationFlags,
|
||||
environment, lpCurrentDirectory, si);
|
||||
}
|
||||
// Zuul: This exception handler is new so that we can
|
||||
// output a log line with a result code in case a
|
||||
// command is not found.
|
||||
catch (Win32Exception e)
|
||||
{
|
||||
using(ZuulConsole console = new ZuulConsole(zuulLogId, zuulLogPath))
|
||||
{
|
||||
console.LogExitCode((UInt32)e.NativeErrorCode);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (isConsole)
|
||||
NativeMethods.FreeConsole();
|
||||
}
|
||||
|
||||
using (pi)
|
||||
{
|
||||
return WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi,
|
||||
outputEncoding, waitChildren, zuulLogId, zuulLogPath, zuulOutputMaxBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper around the Win32 CreateProcess API for low level use. This just spawns the new process and does not
|
||||
/// wait until it is complete before returning.
|
||||
/// </summary>
|
||||
/// <param name="applicationName">The name of the executable or batch file to execute</param>
|
||||
/// <param name="commandLine">The command line to execute, typically this includes applicationName as the first argument</param>
|
||||
/// <param name="processAttributes">SecurityAttributes to assign to the new process, set to null to use the defaults</param>
|
||||
/// <param name="threadAttributes">SecurityAttributes to assign to the new thread, set to null to use the defaults</param>
|
||||
/// <param name="inheritHandles">Any inheritable handles in the calling process is inherited in the new process</param>
|
||||
/// <param name="creationFlags">Custom creation flags to use when creating the new process</param>
|
||||
/// <param name="environment">A dictionary of key/value pairs to define the new process environment</param>
|
||||
/// <param name="currentDirectory">The full path to the current directory for the process, null will have the same cwd as the calling process</param>
|
||||
/// <param name="startupInfo">Custom StartupInformation to use when creating the new process</param>
|
||||
/// <returns>ProcessInformation containing a handle to the process and main thread as well as the pid/tid.</returns>
|
||||
public static ProcessInformation NativeCreateProcess(string applicationName, string commandLine,
|
||||
SecurityAttributes processAttributes, SecurityAttributes threadAttributes, bool inheritHandles,
|
||||
ProcessCreationFlags creationFlags, IDictionary environment, string currentDirectory, StartupInfo startupInfo)
|
||||
{
|
||||
// We always have the extended version present.
|
||||
creationFlags |= ProcessCreationFlags.ExtendedStartupInfoPresent;
|
||||
|
||||
// $null from PowerShell ends up as an empty string, we need to convert back as an empty string doesn't
|
||||
// make sense for these parameters
|
||||
if (String.IsNullOrWhiteSpace(applicationName))
|
||||
applicationName = null;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(currentDirectory))
|
||||
currentDirectory = null;
|
||||
|
||||
NativeHelpers.STARTUPINFOEX si = new NativeHelpers.STARTUPINFOEX();
|
||||
if (!String.IsNullOrWhiteSpace(startupInfo.Desktop))
|
||||
si.startupInfo.lpDesktop = startupInfo.Desktop;
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(startupInfo.Title))
|
||||
si.startupInfo.lpTitle = startupInfo.Title;
|
||||
|
||||
if (startupInfo.WindowStyle != null)
|
||||
{
|
||||
switch (startupInfo.WindowStyle)
|
||||
{
|
||||
case ProcessWindowStyle.Normal:
|
||||
si.startupInfo.wShowWindow = 1; // SW_SHOWNORMAL
|
||||
break;
|
||||
case ProcessWindowStyle.Hidden:
|
||||
si.startupInfo.wShowWindow = 0; // SW_HIDE
|
||||
break;
|
||||
case ProcessWindowStyle.Minimized:
|
||||
si.startupInfo.wShowWindow = 6; // SW_MINIMIZE
|
||||
break;
|
||||
case ProcessWindowStyle.Maximized:
|
||||
si.startupInfo.wShowWindow = 3; // SW_MAXIMIZE
|
||||
break;
|
||||
}
|
||||
si.startupInfo.dwFlags |= NativeHelpers.StartupInfoFlags.STARTF_USESHOWWINDOW;
|
||||
}
|
||||
|
||||
si.lpAttributeList = CreateProcThreadAttributes(startupInfo);
|
||||
|
||||
NativeHelpers.PROCESS_INFORMATION pi = new NativeHelpers.PROCESS_INFORMATION();
|
||||
using (SafeHandle stdinHandle = PrepareStdioHandle(startupInfo.StandardInput, startupInfo))
|
||||
using (SafeHandle stdoutHandle = PrepareStdioHandle(startupInfo.StandardOutput, startupInfo))
|
||||
using (SafeHandle stderrHandle = PrepareStdioHandle(startupInfo.StandardError, startupInfo))
|
||||
using (SafeMemoryBuffer lpProcessAttr = CreateSecurityAttributes(processAttributes))
|
||||
using (SafeMemoryBuffer lpThreadAttributes = CreateSecurityAttributes(threadAttributes))
|
||||
using (SafeMemoryBuffer lpEnvironment = CreateEnvironmentPointer(environment))
|
||||
{
|
||||
si.startupInfo.hStdInput = stdinHandle;
|
||||
si.startupInfo.hStdOutput = stdoutHandle;
|
||||
si.startupInfo.hStdError = stderrHandle;
|
||||
if (
|
||||
si.startupInfo.hStdInput.DangerousGetHandle() != IntPtr.Zero ||
|
||||
si.startupInfo.hStdOutput.DangerousGetHandle() != IntPtr.Zero ||
|
||||
si.startupInfo.hStdError.DangerousGetHandle() != IntPtr.Zero
|
||||
)
|
||||
{
|
||||
si.startupInfo.dwFlags |= NativeHelpers.StartupInfoFlags.USESTDHANDLES;
|
||||
}
|
||||
|
||||
StringBuilder commandLineBuff = new StringBuilder(commandLine);
|
||||
if (!NativeMethods.CreateProcessW(applicationName, commandLineBuff, lpProcessAttr, lpThreadAttributes,
|
||||
inheritHandles, creationFlags, lpEnvironment, currentDirectory, si, out pi))
|
||||
{
|
||||
throw new Win32Exception("CreateProcessW() failed");
|
||||
}
|
||||
}
|
||||
|
||||
return new ProcessInformation
|
||||
{
|
||||
Process = new SafeNativeHandle(pi.hProcess),
|
||||
Thread = new SafeNativeHandle(pi.hThread),
|
||||
ProcessId = pi.dwProcessId,
|
||||
ThreadId = pi.dwThreadId,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resume a suspended thread.
|
||||
/// </summary>
|
||||
/// <param name="thread">The thread handle to resume</param>
|
||||
public static void ResumeThread(SafeHandle thread)
|
||||
{
|
||||
if (NativeMethods.ResumeThread(thread) == 0xFFFFFFFF)
|
||||
throw new Win32Exception("ResumeThread() failed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exit code for the specified process handle.
|
||||
/// </summary>
|
||||
/// <param name="processHandle">The process handle to get the exit code for.</param>
|
||||
/// <returns>The process exit code.</returns>
|
||||
public static UInt32 GetProcessExitCode(SafeHandle processHandle)
|
||||
{
|
||||
NativeMethods.WaitForSingleObject(processHandle, 0xFFFFFFFF);
|
||||
|
||||
UInt32 exitCode;
|
||||
if (!NativeMethods.GetExitCodeProcess(processHandle, out exitCode))
|
||||
throw new Win32Exception("GetExitCodeProcess() failed");
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
internal static void CreateStdioPipes(StartupInfo si, out SafeFileHandle stdoutRead,
|
||||
out SafeFileHandle stdoutWrite, out SafeFileHandle stderrRead, out SafeFileHandle stderrWrite,
|
||||
out SafeFileHandle stdinRead, out SafeFileHandle stdinWrite)
|
||||
{
|
||||
NativeHelpers.SECURITY_ATTRIBUTES pipesec = new NativeHelpers.SECURITY_ATTRIBUTES();
|
||||
pipesec.bInheritHandle = true;
|
||||
|
||||
if (!NativeMethods.CreatePipe(out stdoutRead, out stdoutWrite, pipesec, 0))
|
||||
throw new Win32Exception("STDOUT pipe setup failed");
|
||||
if (!NativeMethods.SetHandleInformation(stdoutRead, NativeHelpers.HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDOUT pipe handle setup failed");
|
||||
|
||||
if (!NativeMethods.CreatePipe(out stderrRead, out stderrWrite, pipesec, 0))
|
||||
throw new Win32Exception("STDERR pipe setup failed");
|
||||
if (!NativeMethods.SetHandleInformation(stderrRead, NativeHelpers.HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDERR pipe handle setup failed");
|
||||
|
||||
if (!NativeMethods.CreatePipe(out stdinRead, out stdinWrite, pipesec, 0))
|
||||
throw new Win32Exception("STDIN pipe setup failed");
|
||||
if (!NativeMethods.SetHandleInformation(stdinWrite, NativeHelpers.HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDIN pipe handle setup failed");
|
||||
|
||||
si.StandardOutput = stdoutWrite;
|
||||
si.StandardError = stderrWrite;
|
||||
si.StandardInput = stdinRead;
|
||||
}
|
||||
|
||||
internal static SafeMemoryBuffer CreateEnvironmentPointer(IDictionary environment)
|
||||
{
|
||||
IntPtr lpEnvironment = IntPtr.Zero;
|
||||
if (environment != null && environment.Count > 0)
|
||||
{
|
||||
StringBuilder environmentString = new StringBuilder();
|
||||
foreach (DictionaryEntry kv in environment)
|
||||
environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value);
|
||||
environmentString.Append('\0');
|
||||
|
||||
lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString());
|
||||
}
|
||||
return new SafeMemoryBuffer(lpEnvironment);
|
||||
}
|
||||
|
||||
internal static SafeMemoryBuffer CreateSecurityAttributes(SecurityAttributes attributes)
|
||||
{
|
||||
IntPtr lpAttributes = IntPtr.Zero;
|
||||
if (attributes != null)
|
||||
{
|
||||
NativeHelpers.SECURITY_ATTRIBUTES attr = new NativeHelpers.SECURITY_ATTRIBUTES()
|
||||
{
|
||||
bInheritHandle = attributes.InheritHandle,
|
||||
};
|
||||
|
||||
lpAttributes = Marshal.AllocHGlobal(Marshal.SizeOf(attr));
|
||||
Marshal.StructureToPtr(attr, lpAttributes, false);
|
||||
}
|
||||
|
||||
return new SafeMemoryBuffer(lpAttributes);
|
||||
}
|
||||
|
||||
internal static SafeDuplicateHandle DuplicateHandle(SafeHandle sourceProcess, SafeHandle sourceHandle,
|
||||
SafeHandle targetProcess, UInt32 access, bool inherit, NativeHelpers.DuplicateHandleOptions options,
|
||||
bool ownsHandle)
|
||||
{
|
||||
if (targetProcess == null)
|
||||
{
|
||||
targetProcess = new SafeNativeHandle(IntPtr.Zero, false);
|
||||
// If closing the duplicate then mark the returned handle so it doesn't try to close itself again.
|
||||
ownsHandle = (options & NativeHelpers.DuplicateHandleOptions.DUPLICATE_CLOSE_SOURCE) == 0;
|
||||
}
|
||||
|
||||
IntPtr dup = IntPtr.Zero;
|
||||
if (!NativeMethods.DuplicateHandle(sourceProcess, sourceHandle, targetProcess, out dup, access,
|
||||
inherit, options))
|
||||
{
|
||||
throw new Win32Exception("DuplicateHandle() failed");
|
||||
}
|
||||
|
||||
return new SafeDuplicateHandle(dup, targetProcess, ownsHandle);
|
||||
}
|
||||
|
||||
internal static Result WaitProcess(
|
||||
SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead,
|
||||
SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, ProcessInformation pi,
|
||||
string outputEncoding, bool waitChildren, string zuulLogId, string zuulLogPath, UInt32 zuulOutputMaxBytes)
|
||||
{
|
||||
// Default to using UTF-8 as the output encoding, this should be a sane default for most scenarios.
|
||||
outputEncoding = String.IsNullOrEmpty(outputEncoding) ? "utf-8" : outputEncoding;
|
||||
Encoding encodingInstance = Encoding.GetEncoding(outputEncoding);
|
||||
|
||||
// If we aren't waiting for child processes we don't care if the below fails
|
||||
// Logic to wait for children is from Raymond Chen
|
||||
// https://devblogs.microsoft.com/oldnewthing/20130405-00/?p=4743
|
||||
using (SafeHandle job = CreateJob(!waitChildren))
|
||||
using (SafeHandle ioPort = CreateCompletionPort(!waitChildren))
|
||||
{
|
||||
// Need to assign the completion port to the job and then assigned the new process to that job.
|
||||
if (waitChildren)
|
||||
{
|
||||
NativeHelpers.JOBOBJECT_ASSOCIATE_COMPLETION_PORT compPort = new NativeHelpers.JOBOBJECT_ASSOCIATE_COMPLETION_PORT()
|
||||
{
|
||||
CompletionKey = job.DangerousGetHandle(),
|
||||
CompletionPort = ioPort.DangerousGetHandle(),
|
||||
};
|
||||
int compPortSize = Marshal.SizeOf(compPort);
|
||||
|
||||
using (SafeMemoryBuffer compPortPtr = new SafeMemoryBuffer(compPortSize))
|
||||
{
|
||||
Marshal.StructureToPtr(compPort, compPortPtr.DangerousGetHandle(), false);
|
||||
|
||||
if (!NativeMethods.SetInformationJobObject(job,
|
||||
NativeHelpers.JobObjectInformationClass.JobObjectAssociateCompletionPortInformation,
|
||||
compPortPtr.DangerousGetHandle(), compPortSize))
|
||||
{
|
||||
throw new Win32Exception("Failed to set job completion port information");
|
||||
}
|
||||
}
|
||||
|
||||
// Server 2012/Win 8 introduced the ability to nest jobs. Older versions will fail with
|
||||
// ERROR_ACCESS_DENIED but we can't do anything about that except not wait for children.
|
||||
if (!NativeMethods.AssignProcessToJobObject(job, pi.Process.DangerousGetHandle()))
|
||||
throw new Win32Exception("Failed to assign new process to completion watcher job");
|
||||
}
|
||||
|
||||
// Start the process and get the output.
|
||||
ResumeThread(pi.Thread);
|
||||
|
||||
FileStream stdoutFS = new FileStream(stdoutRead, FileAccess.Read, 4096);
|
||||
StreamReader stdout = new ZuulStreamReader(stdoutFS, encodingInstance, true, 4096);
|
||||
stdoutWrite.Close();
|
||||
|
||||
FileStream stderrFS = new FileStream(stderrRead, FileAccess.Read, 4096);
|
||||
StreamReader stderr = new ZuulStreamReader(stderrFS, encodingInstance, true, 4096);
|
||||
stderrWrite.Close();
|
||||
|
||||
if (stdin != null)
|
||||
stdinStream.Write(stdin, 0, stdin.Length);
|
||||
stdinStream.Close();
|
||||
|
||||
string stdoutStr, stderrStr = null;
|
||||
// Zuul: We add the process argument here so that our
|
||||
// follower can kill the process if it emits too much
|
||||
// output.
|
||||
GetProcessOutput(stdout, stderr, pi.Process, zuulLogId, zuulLogPath, zuulOutputMaxBytes, out stdoutStr, out stderrStr);
|
||||
UInt32 rc = GetProcessExitCode(pi.Process);
|
||||
|
||||
if (waitChildren)
|
||||
{
|
||||
// If the caller wants to wait for all child processes to finish, we continue to poll the job
|
||||
// until it receives JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO (4).
|
||||
UInt32 completionCode = 0xFFFFFFFF;
|
||||
UIntPtr completionKey;
|
||||
IntPtr overlapped;
|
||||
|
||||
while (NativeMethods.GetQueuedCompletionStatus(ioPort, out completionCode,
|
||||
out completionKey, out overlapped, 0xFFFFFFFF) && completionCode != 4) { }
|
||||
}
|
||||
|
||||
return new Result
|
||||
{
|
||||
StandardOut = stdoutStr,
|
||||
StandardError = stderrStr,
|
||||
ExitCode = rc
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Zuul: This method replaces the original
|
||||
internal static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, SafeNativeHandle process, string zuulLogId, string zuulLogPath, UInt32 zuulOutputMaxBytes, out string stdout, out string stderr)
|
||||
{
|
||||
StreamFollower sf = new StreamFollower(process, stdoutStream, stderrStream, zuulLogId, zuulLogPath, zuulOutputMaxBytes);
|
||||
sf.Follow();
|
||||
sf.Join();
|
||||
stdout = sf.outLogBytes.ToString();
|
||||
stderr = sf.errLogBytes.ToString();
|
||||
sf.LogExitCode(GetProcessExitCode(process));
|
||||
sf.Dispose();
|
||||
}
|
||||
|
||||
private static SafeHandle CreateJob(bool ignoreErrors)
|
||||
{
|
||||
SafeNativeHandle job = NativeMethods.CreateJobObjectW(IntPtr.Zero, null);
|
||||
if (job.IsInvalid && !ignoreErrors)
|
||||
throw new Win32Exception("Failed to create job object");
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
private static SafeHandle CreateCompletionPort(bool ignoreErrors)
|
||||
{
|
||||
SafeNativeHandle ioPort = NativeMethods.CreateIoCompletionPort((IntPtr)(-1), IntPtr.Zero,
|
||||
UIntPtr.Zero, 1);
|
||||
|
||||
if (ioPort.IsInvalid && !ignoreErrors)
|
||||
throw new Win32Exception("Failed to create IoCompletionPort");
|
||||
|
||||
return ioPort;
|
||||
}
|
||||
|
||||
private static SafeHandle CreateProcThreadAttributes(StartupInfo startupInfo)
|
||||
{
|
||||
int count = 0;
|
||||
if (startupInfo.ParentProcess > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return new SafeNativeHandle(IntPtr.Zero, false);
|
||||
}
|
||||
|
||||
SafeProcThreadAttribute attr = InitializeProcThreadAttributeList(count);
|
||||
try
|
||||
{
|
||||
if (startupInfo.ParentProcess > 0)
|
||||
{
|
||||
SafeNativeHandle parentProcess = OpenProcess(startupInfo.ParentProcess,
|
||||
0x00000080, // PROCESS_CREATE_PROCESS
|
||||
false);
|
||||
attr.AddValue(parentProcess);
|
||||
|
||||
SafeMemoryBuffer val = new SafeMemoryBuffer(IntPtr.Size);
|
||||
attr.AddValue(val);
|
||||
|
||||
Marshal.WriteIntPtr(val.DangerousGetHandle(), parentProcess.DangerousGetHandle());
|
||||
UpdateProcThreadAttribute(attr,
|
||||
0x00020000, // PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
|
||||
val,
|
||||
IntPtr.Size);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
attr.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
return attr;
|
||||
}
|
||||
|
||||
private static SafeProcThreadAttribute InitializeProcThreadAttributeList(int count)
|
||||
{
|
||||
IntPtr size = IntPtr.Zero;
|
||||
NativeMethods.InitializeProcThreadAttributeList(IntPtr.Zero, count, 0, ref size);
|
||||
|
||||
IntPtr h = Marshal.AllocHGlobal((int)size);
|
||||
try
|
||||
{
|
||||
if (!NativeMethods.InitializeProcThreadAttributeList(h, count, 0, ref size))
|
||||
throw new Win32Exception("Failed to create process thread attribute list");
|
||||
|
||||
return new SafeProcThreadAttribute(h, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Marshal.FreeHGlobal(h);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static SafeNativeHandle OpenProcess(int processId, int access, bool inherit)
|
||||
{
|
||||
SafeNativeHandle proc = NativeMethods.OpenProcess(access, inherit, processId);
|
||||
if (proc.DangerousGetHandle() == IntPtr.Zero)
|
||||
{
|
||||
throw new Win32Exception(string.Format(
|
||||
"OpenProcess(0x{0:X8}, {1}, {2}) failed",
|
||||
access, inherit, processId));
|
||||
}
|
||||
|
||||
return proc;
|
||||
}
|
||||
|
||||
private static SafeHandle PrepareStdioHandle(SafeHandle handle, StartupInfo startupInfo)
|
||||
{
|
||||
if (handle == null || handle.DangerousGetHandle() == IntPtr.Zero)
|
||||
return new SafeNativeHandle(IntPtr.Zero, false);
|
||||
|
||||
if (startupInfo.ParentProcess > 0)
|
||||
{
|
||||
// The handle needs to be duplicated into the target process so
|
||||
// it can be inherited.
|
||||
SafeNativeHandle currentProcess = new SafeNativeHandle(NativeMethods.GetCurrentProcess(), false);
|
||||
SafeNativeHandle targetProcess = OpenProcess(startupInfo.ParentProcess,
|
||||
0x00000040, // PROCESS_DUP_HANDLE
|
||||
false);
|
||||
|
||||
return DuplicateHandle(currentProcess, handle, targetProcess, 0, true,
|
||||
NativeHelpers.DuplicateHandleOptions.DUPLICATE_SAME_ACCESS, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a copy of the handle and ensure it won't be disposed.
|
||||
// The original owner is still in charge of it.
|
||||
return new SafeNativeHandle(handle.DangerousGetHandle(), false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateProcThreadAttribute(SafeProcThreadAttribute attributeList, int attr,
|
||||
SafeHandle value, int size)
|
||||
{
|
||||
if (!NativeMethods.UpdateProcThreadAttribute(attributeList, 0, (UIntPtr)attr, value, (UIntPtr)size,
|
||||
IntPtr.Zero, IntPtr.Zero))
|
||||
{
|
||||
throw new Win32Exception("UpdateProcThreadAttribute() failed");
|
||||
}
|
||||
|
||||
attributeList.AddValue(value);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,308 @@
|
||||
// Copyright (c) 2016 IBM Corp.
|
||||
// Copyright (C) 2025 Acme Gating, LLC
|
||||
//
|
||||
// This module is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This software is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System.Runtime.InteropServices;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
|
||||
//AssemblyReference -Name netstandard.dll
|
||||
//AssemblyReference -Name System.Xml.dll
|
||||
//AssemblyReference -Name System.Xml.ReaderWriter.dll
|
||||
|
||||
namespace Ansible.Zuul.Win.Common
|
||||
{
|
||||
internal class NativeMethods
|
||||
{
|
||||
// The collections Process file includes this, but not the
|
||||
// core Process file, so we go aheand and import it ourselves.
|
||||
// From
|
||||
// https://github.com/ansible-collections/ansible.windows/blob/2.5.0/plugins/module_utils/Process.cs
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern bool TerminateProcess(
|
||||
SafeHandle hProcess,
|
||||
UInt32 lpExitCode);
|
||||
}
|
||||
|
||||
// From
|
||||
// https://github.com/ansible-collections/ansible.windows/blob/2.5.0/plugins/module_utils/Process.cs
|
||||
public class Win32Exception : System.ComponentModel.Win32Exception
|
||||
{
|
||||
private string _msg;
|
||||
|
||||
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
||||
public Win32Exception(int errorCode, string message) : base(errorCode)
|
||||
{
|
||||
_msg = String.Format("{0} ({1}, Win32ErrorCode {2} - 0x{2:X8})", message, base.Message, errorCode);
|
||||
}
|
||||
|
||||
public override string Message { get { return _msg; } }
|
||||
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
|
||||
}
|
||||
|
||||
public class ZuulStreamReader : StreamReader
|
||||
{
|
||||
private StringBuilder sb = new StringBuilder();
|
||||
|
||||
public ZuulStreamReader(Stream stream, Encoding enc, bool order, Int32 size) : base(stream, enc, order, size) {}
|
||||
|
||||
// This overrides the base class to include the newline sequence.
|
||||
public override string ReadLine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
int c = Read();
|
||||
if (c == -1)
|
||||
{
|
||||
if (sb.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string ret = sb.ToString();
|
||||
sb = new StringBuilder();
|
||||
return ret;
|
||||
}
|
||||
char ch = (char)c;
|
||||
sb.Append(ch);
|
||||
if (ch == '\r' && Peek() == '\n')
|
||||
{
|
||||
sb.Append((char)Read());
|
||||
}
|
||||
if (ch == '\r' || ch == '\n')
|
||||
{
|
||||
string ret = sb.ToString();
|
||||
sb = new StringBuilder();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class ZuulConsole : IDisposable
|
||||
{
|
||||
private StreamWriter logFile;
|
||||
|
||||
public ZuulConsole(string zuulLogId, string zuulLogPath)
|
||||
{
|
||||
if (zuulLogId == "in-loop-ignore")
|
||||
{
|
||||
logFile = null;
|
||||
}
|
||||
else if (zuulLogId == "skip")
|
||||
{
|
||||
logFile = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(zuulLogPath));
|
||||
logFile = new StreamWriter(zuulLogPath);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (logFile != null)
|
||||
{
|
||||
logFile.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddLine(string ln)
|
||||
{
|
||||
// Note this format with deliminator is "inspired" by the old
|
||||
// Jenkins format but with microsecond resolution instead of
|
||||
// millisecond. It is kept so log parsing/formatting remains
|
||||
// consistent.
|
||||
if (logFile != null) {
|
||||
lock(logFile) {
|
||||
string ts = DateTime.Now.ToString("yyy-MM-dd HH:mm:ss.ffffff");
|
||||
string outln = string.Format("{0} | {1}", ts, ln);
|
||||
logFile.Write(outln);
|
||||
logFile.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
public void LogExitCode(UInt32 rc)
|
||||
{
|
||||
AddLine(string.Format("[Zuul] Task exit code: {0}\n", rc));
|
||||
}
|
||||
}
|
||||
internal class StreamFollower : IDisposable
|
||||
{
|
||||
// Unlike the python equivalent, we never supported combining
|
||||
// output and error streams, so this class always uses both.
|
||||
private SafeHandle process;
|
||||
private string zuulLogId;
|
||||
private string zuulLogPath;
|
||||
private StreamReader outStream;
|
||||
private StreamReader errStream;
|
||||
private UInt32 outputMaxBytes;
|
||||
// Lists to save stdout/stderr log lines in as we collect them
|
||||
public StringBuilder outLogBytes;
|
||||
public StringBuilder errLogBytes;
|
||||
private Thread stdoutThread;
|
||||
private Thread stderrThread;
|
||||
// Total size in bytes of all log and stderr_log lines
|
||||
private UInt32 logSize;
|
||||
private ZuulConsole console;
|
||||
private static Regex cliXmlRegex = new Regex("(?<clixml><Objs.+</Objs>)(?<post>.*)");
|
||||
|
||||
public StreamFollower(SafeHandle process, StreamReader outStream,
|
||||
StreamReader errStream, string zuulLogId,
|
||||
string zuulLogPath, UInt32 outputMaxBytes)
|
||||
{
|
||||
this.process = process;
|
||||
this.zuulLogId = zuulLogId;
|
||||
this.zuulLogPath = zuulLogPath;
|
||||
this.outLogBytes = new StringBuilder();
|
||||
this.errLogBytes = new StringBuilder();
|
||||
this.outStream = outStream;
|
||||
this.errStream = errStream;
|
||||
this.outputMaxBytes = outputMaxBytes;
|
||||
this.logSize = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (console != null) {
|
||||
console.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Follow()
|
||||
{
|
||||
console = new ZuulConsole(zuulLogId, zuulLogPath);
|
||||
stdoutThread = new Thread(this.FollowOut);
|
||||
stdoutThread.Start();
|
||||
stderrThread = new Thread(this.FollowErr);
|
||||
stderrThread.Start();
|
||||
}
|
||||
|
||||
private void FollowOut()
|
||||
{
|
||||
FollowInner(outStream, outLogBytes);
|
||||
}
|
||||
|
||||
private void FollowErr()
|
||||
{
|
||||
FollowInner(errStream, errLogBytes);
|
||||
}
|
||||
|
||||
|
||||
private void FollowInner(StreamReader stream, StringBuilder logBytes)
|
||||
{
|
||||
// The unix/python version of this has a check that throws
|
||||
// a warning if we encounter a line without a trailing
|
||||
// newline. That doesn't make as much sense here, so we
|
||||
// omit it.
|
||||
|
||||
// These variables are used for the CLIXML handling
|
||||
// (win_shell only).
|
||||
bool first = true;
|
||||
bool cliXml = false;
|
||||
while (true)
|
||||
{
|
||||
string line = stream.ReadLine();
|
||||
if (line == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
logSize += (UInt32) line.Length;
|
||||
if (logSize > outputMaxBytes)
|
||||
{
|
||||
string msg = string.Format("[Zuul] Log output exceeded max of {0}, terminating\n", outputMaxBytes);
|
||||
console.AddLine(msg);
|
||||
|
||||
if (!NativeMethods.TerminateProcess(process, 1))
|
||||
{
|
||||
throw new Win32Exception("TerminateProcess() failed");
|
||||
}
|
||||
throw new Win32Exception(msg);
|
||||
}
|
||||
logBytes.Append(line);
|
||||
|
||||
// Begin CLIXML handling section (win_shell only)
|
||||
|
||||
// This attempts to approximate the way the win_shell
|
||||
// module handles the output in win_shell.ps1.
|
||||
// Because the module handles the returned structured
|
||||
// output, we only apply this to the streaming logs.
|
||||
if (stream == errStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
if (line.Equals("#< CLIXML"))
|
||||
{
|
||||
// Switch on CLIXML handling for this stream.
|
||||
cliXml = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (cliXml)
|
||||
{
|
||||
Match m = cliXmlRegex.Match(line);
|
||||
if (m.Success)
|
||||
{
|
||||
XmlDocument xml = new XmlDocument();
|
||||
xml.LoadXml(m.Groups["clixml"].Value);
|
||||
|
||||
XmlNode root = xml.DocumentElement;
|
||||
XmlNodeList nodes = root.SelectNodes("*[local-name()='S' and @S='Error']");
|
||||
|
||||
foreach (XmlNode node in nodes)
|
||||
{
|
||||
console.AddLine(node.InnerText.Replace("_x000D__x000A_", "") + "\n");
|
||||
}
|
||||
if (m.Groups["post"].Value.Length > 0)
|
||||
{
|
||||
console.AddLine(m.Groups["post"].Value + "\n");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (Exception e)
|
||||
{
|
||||
console.AddLine("[Zuul] Error handling CLIXML: " + e.ToString() + "\n");
|
||||
}
|
||||
}
|
||||
// End CLIXML handling section
|
||||
console.AddLine(line);
|
||||
}
|
||||
}
|
||||
public void Join()
|
||||
{
|
||||
foreach (var thread in new Thread[] { stdoutThread, stderrThread })
|
||||
{
|
||||
// The original win_command will wait until the output
|
||||
// streams are closed before waiting for the process
|
||||
// to exit, so unlike the unix/python version, we
|
||||
// don't timeout the join.
|
||||
thread.Join();
|
||||
}
|
||||
}
|
||||
public void LogExitCode(UInt32 rc)
|
||||
{
|
||||
console.LogExitCode(rc);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,506 @@
|
||||
// This is based on: https://github.com/ansible/ansible/blob/v2.16.14/lib/ansible/module_utils/csharp/Ansible.Process.cs
|
||||
// That file does not have a license header, so it is presumed to use
|
||||
// the GPLv3 as noted in the root of the repo.
|
||||
// GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
// Zuul note: This file, originating in ansible-core, is similar to a
|
||||
// file in the ansible.windows collection, but this file is only used
|
||||
// by the win_shell module while the win_command module uses the
|
||||
// version in the collection. Therefore, in Zuul, this file and
|
||||
// namespace have adopted the name "Ansible.Zuul.Win.Shell.Process" to
|
||||
// delineate that this is the Zuul version of the Process module used
|
||||
// by win_shell.
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
using Ansible.Zuul.Win.Common;
|
||||
|
||||
namespace Ansible.Zuul.Win.Shell.Process
|
||||
{
|
||||
internal class NativeHelpers
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class SECURITY_ATTRIBUTES
|
||||
{
|
||||
public UInt32 nLength;
|
||||
public IntPtr lpSecurityDescriptor;
|
||||
public bool bInheritHandle = false;
|
||||
public SECURITY_ATTRIBUTES()
|
||||
{
|
||||
nLength = (UInt32)Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class STARTUPINFO
|
||||
{
|
||||
public UInt32 cb;
|
||||
public IntPtr lpReserved;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string lpDesktop;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string lpTitle;
|
||||
public UInt32 dwX;
|
||||
public UInt32 dwY;
|
||||
public UInt32 dwXSize;
|
||||
public UInt32 dwYSize;
|
||||
public UInt32 dwXCountChars;
|
||||
public UInt32 dwYCountChars;
|
||||
public UInt32 dwFillAttribute;
|
||||
public StartupInfoFlags dwFlags;
|
||||
public UInt16 wShowWindow;
|
||||
public UInt16 cbReserved2;
|
||||
public IntPtr lpReserved2;
|
||||
public SafeFileHandle hStdInput;
|
||||
public SafeFileHandle hStdOutput;
|
||||
public SafeFileHandle hStdError;
|
||||
public STARTUPINFO()
|
||||
{
|
||||
cb = (UInt32)Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class STARTUPINFOEX
|
||||
{
|
||||
public STARTUPINFO startupInfo;
|
||||
public IntPtr lpAttributeList;
|
||||
public STARTUPINFOEX()
|
||||
{
|
||||
startupInfo = new STARTUPINFO();
|
||||
startupInfo.cb = (UInt32)Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct PROCESS_INFORMATION
|
||||
{
|
||||
public IntPtr hProcess;
|
||||
public IntPtr hThread;
|
||||
public int dwProcessId;
|
||||
public int dwThreadId;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ProcessCreationFlags : uint
|
||||
{
|
||||
CREATE_NEW_CONSOLE = 0x00000010,
|
||||
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
|
||||
EXTENDED_STARTUPINFO_PRESENT = 0x00080000
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum StartupInfoFlags : uint
|
||||
{
|
||||
USESTDHANDLES = 0x00000100
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum HandleFlags : uint
|
||||
{
|
||||
None = 0,
|
||||
INHERIT = 1
|
||||
}
|
||||
}
|
||||
|
||||
internal class NativeMethods
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool AllocConsole();
|
||||
|
||||
[DllImport("shell32.dll", SetLastError = true)]
|
||||
public static extern SafeMemoryBuffer CommandLineToArgvW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
|
||||
out int pNumArgs);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool CreatePipe(
|
||||
out SafeFileHandle hReadPipe,
|
||||
out SafeFileHandle hWritePipe,
|
||||
NativeHelpers.SECURITY_ATTRIBUTES lpPipeAttributes,
|
||||
UInt32 nSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern bool CreateProcessW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName,
|
||||
StringBuilder lpCommandLine,
|
||||
IntPtr lpProcessAttributes,
|
||||
IntPtr lpThreadAttributes,
|
||||
bool bInheritHandles,
|
||||
NativeHelpers.ProcessCreationFlags dwCreationFlags,
|
||||
SafeMemoryBuffer lpEnvironment,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory,
|
||||
NativeHelpers.STARTUPINFOEX lpStartupInfo,
|
||||
out NativeHelpers.PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool FreeConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetConsoleWindow();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool GetExitCodeProcess(
|
||||
SafeWaitHandle hProcess,
|
||||
out UInt32 lpExitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern uint SearchPathW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpPath,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpExtension,
|
||||
UInt32 nBufferLength,
|
||||
[MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpBuffer,
|
||||
out IntPtr lpFilePart);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetConsoleCP(
|
||||
UInt32 wCodePageID);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetConsoleOutputCP(
|
||||
UInt32 wCodePageID);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetHandleInformation(
|
||||
SafeFileHandle hObject,
|
||||
NativeHelpers.HandleFlags dwMask,
|
||||
NativeHelpers.HandleFlags dwFlags);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern UInt32 WaitForSingleObject(
|
||||
SafeWaitHandle hHandle,
|
||||
UInt32 dwMilliseconds);
|
||||
}
|
||||
|
||||
internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
public SafeMemoryBuffer() : base(true) { }
|
||||
public SafeMemoryBuffer(int cb) : base(true)
|
||||
{
|
||||
base.SetHandle(Marshal.AllocHGlobal(cb));
|
||||
}
|
||||
public SafeMemoryBuffer(IntPtr handle) : base(true)
|
||||
{
|
||||
base.SetHandle(handle);
|
||||
}
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
Marshal.FreeHGlobal(handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class Win32Exception : System.ComponentModel.Win32Exception
|
||||
{
|
||||
private string _msg;
|
||||
|
||||
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
||||
public Win32Exception(int errorCode, string message) : base(errorCode)
|
||||
{
|
||||
_msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
|
||||
}
|
||||
|
||||
public override string Message { get { return _msg; } }
|
||||
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
|
||||
}
|
||||
|
||||
public class Result
|
||||
{
|
||||
public string StandardOut { get; internal set; }
|
||||
public string StandardError { get; internal set; }
|
||||
public uint ExitCode { get; internal set; }
|
||||
}
|
||||
|
||||
public class ProcessUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a command line string into an argv array according to the Windows rules
|
||||
/// </summary>
|
||||
/// <param name="lpCommandLine">The command line to parse</param>
|
||||
/// <returns>An array of arguments interpreted by Windows</returns>
|
||||
public static string[] ParseCommandLine(string lpCommandLine)
|
||||
{
|
||||
int numArgs;
|
||||
using (SafeMemoryBuffer buf = NativeMethods.CommandLineToArgvW(lpCommandLine, out numArgs))
|
||||
{
|
||||
if (buf.IsInvalid)
|
||||
throw new Win32Exception("Error parsing command line");
|
||||
IntPtr[] strptrs = new IntPtr[numArgs];
|
||||
Marshal.Copy(buf.DangerousGetHandle(), strptrs, 0, numArgs);
|
||||
return strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches the path for the executable specified. Will throw a Win32Exception if the file is not found.
|
||||
/// </summary>
|
||||
/// <param name="lpFileName">The executable to search for</param>
|
||||
/// <returns>The full path of the executable to search for</returns>
|
||||
public static string SearchPath(string lpFileName)
|
||||
{
|
||||
StringBuilder sbOut = new StringBuilder(0);
|
||||
IntPtr filePartOut = IntPtr.Zero;
|
||||
UInt32 res = NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut);
|
||||
if (res == 0)
|
||||
{
|
||||
int lastErr = Marshal.GetLastWin32Error();
|
||||
if (lastErr == 2) // ERROR_FILE_NOT_FOUND
|
||||
throw new FileNotFoundException(String.Format("Could not find file '{0}'.", lpFileName));
|
||||
else
|
||||
throw new Win32Exception(String.Format("SearchPathW({0}) failed to get buffer length", lpFileName));
|
||||
}
|
||||
|
||||
sbOut.EnsureCapacity((int)res);
|
||||
if (NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut) == 0)
|
||||
throw new Win32Exception(String.Format("SearchPathW({0}) failed", lpFileName));
|
||||
|
||||
return sbOut.ToString();
|
||||
}
|
||||
|
||||
/* Zuul: none of these are used by win_shell
|
||||
public static Result CreateProcess(string command)
|
||||
{
|
||||
return CreateProcess(null, command, null, null, String.Empty);
|
||||
}
|
||||
|
||||
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||
IDictionary environment)
|
||||
{
|
||||
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, String.Empty);
|
||||
}
|
||||
|
||||
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||
IDictionary environment, string stdin)
|
||||
{
|
||||
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdin, null);
|
||||
}
|
||||
|
||||
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||
IDictionary environment, byte[] stdin)
|
||||
{
|
||||
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdin, null);
|
||||
}
|
||||
|
||||
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||
IDictionary environment, string stdin, string outputEncoding)
|
||||
{
|
||||
byte[] stdinBytes;
|
||||
if (String.IsNullOrEmpty(stdin))
|
||||
stdinBytes = new byte[0];
|
||||
else
|
||||
{
|
||||
if (!stdin.EndsWith(Environment.NewLine))
|
||||
stdin += Environment.NewLine;
|
||||
stdinBytes = new UTF8Encoding(false).GetBytes(stdin);
|
||||
}
|
||||
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdinBytes, outputEncoding);
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a process based on the CreateProcess API call.
|
||||
/// </summary>
|
||||
/// <param name="lpApplicationName">The name of the executable or batch file to execute</param>
|
||||
/// <param name="lpCommandLine">The command line to execute, typically this includes lpApplication as the first argument</param>
|
||||
/// <param name="lpCurrentDirectory">The full path to the current directory for the process, null will have the same cwd as the calling process</param>
|
||||
/// <param name="environment">A dictionary of key/value pairs to define the new process environment</param>
|
||||
/// <param name="stdin">A byte array to send over the stdin pipe</param>
|
||||
/// <param name="outputEncoding">The character encoding for decoding stdout/stderr output of the process.</param>
|
||||
/// <returns>Result object that contains the command output and return code</returns>
|
||||
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||
IDictionary environment, byte[] stdin, string outputEncoding,
|
||||
string zuulLogId, string zuulLogPath, UInt32 zuulOutputMaxBytes)
|
||||
{
|
||||
NativeHelpers.ProcessCreationFlags creationFlags = NativeHelpers.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT |
|
||||
NativeHelpers.ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT;
|
||||
NativeHelpers.PROCESS_INFORMATION pi = new NativeHelpers.PROCESS_INFORMATION();
|
||||
NativeHelpers.STARTUPINFOEX si = new NativeHelpers.STARTUPINFOEX();
|
||||
si.startupInfo.dwFlags = NativeHelpers.StartupInfoFlags.USESTDHANDLES;
|
||||
|
||||
SafeFileHandle stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinRead, stdinWrite;
|
||||
CreateStdioPipes(si, out stdoutRead, out stdoutWrite, out stderrRead, out stderrWrite, out stdinRead,
|
||||
out stdinWrite);
|
||||
FileStream stdinStream = new FileStream(stdinWrite, FileAccess.Write);
|
||||
|
||||
// $null from PowerShell ends up as an empty string, we need to convert back as an empty string doesn't
|
||||
// make sense for these parameters
|
||||
if (lpApplicationName == "")
|
||||
lpApplicationName = null;
|
||||
|
||||
if (lpCurrentDirectory == "")
|
||||
lpCurrentDirectory = null;
|
||||
|
||||
using (SafeMemoryBuffer lpEnvironment = CreateEnvironmentPointer(environment))
|
||||
{
|
||||
// Create console with utf-8 CP if no existing console is present
|
||||
bool isConsole = false;
|
||||
if (NativeMethods.GetConsoleWindow() == IntPtr.Zero)
|
||||
{
|
||||
isConsole = NativeMethods.AllocConsole();
|
||||
|
||||
// Set console input/output codepage to UTF-8
|
||||
NativeMethods.SetConsoleCP(65001);
|
||||
NativeMethods.SetConsoleOutputCP(65001);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StringBuilder commandLine = new StringBuilder(lpCommandLine);
|
||||
if (!NativeMethods.CreateProcessW(lpApplicationName, commandLine, IntPtr.Zero, IntPtr.Zero,
|
||||
true, creationFlags, lpEnvironment, lpCurrentDirectory, si, out pi))
|
||||
{
|
||||
throw new Win32Exception("CreateProcessW() failed");
|
||||
}
|
||||
}
|
||||
// Zuul: This exception handler is new so that we can
|
||||
// output a log line with a result code in case a
|
||||
// command is not found.
|
||||
catch (Win32Exception e)
|
||||
{
|
||||
using(ZuulConsole console = new ZuulConsole(zuulLogId, zuulLogPath))
|
||||
{
|
||||
console.LogExitCode((UInt32)e.NativeErrorCode);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (isConsole)
|
||||
NativeMethods.FreeConsole();
|
||||
}
|
||||
}
|
||||
|
||||
return WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi.hProcess,
|
||||
outputEncoding, zuulLogId, zuulLogPath, zuulOutputMaxBytes);
|
||||
}
|
||||
|
||||
internal static void CreateStdioPipes(NativeHelpers.STARTUPINFOEX si, out SafeFileHandle stdoutRead,
|
||||
out SafeFileHandle stdoutWrite, out SafeFileHandle stderrRead, out SafeFileHandle stderrWrite,
|
||||
out SafeFileHandle stdinRead, out SafeFileHandle stdinWrite)
|
||||
{
|
||||
NativeHelpers.SECURITY_ATTRIBUTES pipesec = new NativeHelpers.SECURITY_ATTRIBUTES();
|
||||
pipesec.bInheritHandle = true;
|
||||
|
||||
if (!NativeMethods.CreatePipe(out stdoutRead, out stdoutWrite, pipesec, 0))
|
||||
throw new Win32Exception("STDOUT pipe setup failed");
|
||||
if (!NativeMethods.SetHandleInformation(stdoutRead, NativeHelpers.HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDOUT pipe handle setup failed");
|
||||
|
||||
if (!NativeMethods.CreatePipe(out stderrRead, out stderrWrite, pipesec, 0))
|
||||
throw new Win32Exception("STDERR pipe setup failed");
|
||||
if (!NativeMethods.SetHandleInformation(stderrRead, NativeHelpers.HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDERR pipe handle setup failed");
|
||||
|
||||
if (!NativeMethods.CreatePipe(out stdinRead, out stdinWrite, pipesec, 0))
|
||||
throw new Win32Exception("STDIN pipe setup failed");
|
||||
if (!NativeMethods.SetHandleInformation(stdinWrite, NativeHelpers.HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDIN pipe handle setup failed");
|
||||
|
||||
si.startupInfo.hStdOutput = stdoutWrite;
|
||||
si.startupInfo.hStdError = stderrWrite;
|
||||
si.startupInfo.hStdInput = stdinRead;
|
||||
}
|
||||
|
||||
internal static SafeMemoryBuffer CreateEnvironmentPointer(IDictionary environment)
|
||||
{
|
||||
IntPtr lpEnvironment = IntPtr.Zero;
|
||||
if (environment != null && environment.Count > 0)
|
||||
{
|
||||
StringBuilder environmentString = new StringBuilder();
|
||||
foreach (DictionaryEntry kv in environment)
|
||||
environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value);
|
||||
environmentString.Append('\0');
|
||||
|
||||
lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString());
|
||||
}
|
||||
return new SafeMemoryBuffer(lpEnvironment);
|
||||
}
|
||||
|
||||
internal static Result WaitProcess(SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead,
|
||||
SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, IntPtr hProcess, string outputEncoding,
|
||||
string zuulLogId, string zuulLogPath, UInt32 zuulOutputMaxBytes)
|
||||
{
|
||||
// Default to using UTF-8 as the output encoding, this should be a sane default for most scenarios.
|
||||
outputEncoding = String.IsNullOrEmpty(outputEncoding) ? "utf-8" : outputEncoding;
|
||||
Encoding encodingInstance = Encoding.GetEncoding(outputEncoding);
|
||||
|
||||
FileStream stdoutFS = new FileStream(stdoutRead, FileAccess.Read, 4096);
|
||||
StreamReader stdout = new ZuulStreamReader(stdoutFS, encodingInstance, true, 4096);
|
||||
stdoutWrite.Close();
|
||||
|
||||
FileStream stderrFS = new FileStream(stderrRead, FileAccess.Read, 4096);
|
||||
StreamReader stderr = new ZuulStreamReader(stderrFS, encodingInstance, true, 4096);
|
||||
stderrWrite.Close();
|
||||
|
||||
stdinStream.Write(stdin, 0, stdin.Length);
|
||||
stdinStream.Close();
|
||||
|
||||
string stdoutStr, stderrStr = null;
|
||||
|
||||
// Zuul: We add the hProcess argument here so that our
|
||||
// follower can kill the process if it emits too much
|
||||
// output.
|
||||
GetProcessOutput(stdout, stderr, hProcess, zuulLogId, zuulLogPath, zuulOutputMaxBytes, out stdoutStr, out stderrStr);
|
||||
UInt32 rc = GetProcessExitCode(hProcess);
|
||||
|
||||
return new Result
|
||||
{
|
||||
StandardOut = stdoutStr,
|
||||
StandardError = stderrStr,
|
||||
ExitCode = rc
|
||||
};
|
||||
}
|
||||
|
||||
// Zuul: This method replaces the original
|
||||
internal static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, IntPtr hProcess, string zuulLogId, string zuulLogPath, UInt32 zuulOutputMaxBytes, out string stdout, out string stderr)
|
||||
{
|
||||
SafeWaitHandle process = new SafeWaitHandle(hProcess, true);
|
||||
StreamFollower sf = new StreamFollower(process, stdoutStream, stderrStream, zuulLogId, zuulLogPath, zuulOutputMaxBytes);
|
||||
sf.Follow();
|
||||
sf.Join();
|
||||
stdout = sf.outLogBytes.ToString();
|
||||
stderr = sf.errLogBytes.ToString();
|
||||
sf.LogExitCode(GetProcessExitCode(hProcess));
|
||||
sf.Dispose();
|
||||
}
|
||||
|
||||
internal static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
|
||||
{
|
||||
var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
string so = null, se = null;
|
||||
ThreadPool.QueueUserWorkItem((s) =>
|
||||
{
|
||||
so = stdoutStream.ReadToEnd();
|
||||
sowait.Set();
|
||||
});
|
||||
ThreadPool.QueueUserWorkItem((s) =>
|
||||
{
|
||||
se = stderrStream.ReadToEnd();
|
||||
sewait.Set();
|
||||
});
|
||||
foreach (var wh in new WaitHandle[] { sowait, sewait })
|
||||
wh.WaitOne();
|
||||
stdout = so;
|
||||
stderr = se;
|
||||
}
|
||||
|
||||
internal static UInt32 GetProcessExitCode(IntPtr processHandle)
|
||||
{
|
||||
SafeWaitHandle hProcess = new SafeWaitHandle(processHandle, true);
|
||||
NativeMethods.WaitForSingleObject(hProcess, 0xFFFFFFFF);
|
||||
|
||||
UInt32 exitCode;
|
||||
if (!NativeMethods.GetExitCodeProcess(hProcess, out exitCode))
|
||||
throw new Win32Exception("GetExitCodeProcess() failed");
|
||||
return exitCode;
|
||||
}
|
||||
}
|
||||
}
|
0
zuul/ansible/base/library/module_utils/__init__.py
Normal file
0
zuul/ansible/base/library/module_utils/__init__.py
Normal file
155
zuul/ansible/base/library/win_command.ps1
Normal file
155
zuul/ansible/base/library/win_command.ps1
Normal file
@ -0,0 +1,155 @@
|
||||
#!powershell
|
||||
|
||||
# This is based on: https://github.com/ansible-collections/ansible.windows/blob/c3f26bb4fa1b3a08ee2c045d2fdd53e1df171d19/plugins/modules/win_command.ps1
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
#Requires -Module Ansible.ModuleUtils.Zuul.Win.Command.Process
|
||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
_raw_params = @{ type = "str" }
|
||||
cmd = @{ type = 'str' }
|
||||
argv = @{ type = "list"; elements = "str" }
|
||||
chdir = @{ type = "path" }
|
||||
creates = @{ type = "path" }
|
||||
removes = @{ type = "path" }
|
||||
stdin = @{ type = "str" }
|
||||
output_encoding_override = @{ type = "str" }
|
||||
zuul_log_id = @{ type = "str" }
|
||||
zuul_output_max_bytes = @{ type = "int" }
|
||||
}
|
||||
required_one_of = @(
|
||||
, @('_raw_params', 'argv', 'cmd')
|
||||
)
|
||||
mutually_exclusive = @(
|
||||
, @('_raw_params', 'argv', 'cmd')
|
||||
)
|
||||
supports_check_mode = $false
|
||||
}
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$chdir = $module.Params.chdir
|
||||
$creates = $module.Params.creates
|
||||
$removes = $module.Params.removes
|
||||
$stdin = $module.Params.stdin
|
||||
$output_encoding_override = $module.Params.output_encoding_override
|
||||
$zuul_log_id = $module.Params.zuul_log_id
|
||||
$zuul_output_max_bytes = $module.Params.zuul_output_max_bytes
|
||||
|
||||
<#
|
||||
There are 3 ways a command can be specified with win_command:
|
||||
|
||||
1. Through _raw_params - the value will be used as is
|
||||
|
||||
- win_command: raw params here
|
||||
|
||||
2. Through cmd - the value will be used as is
|
||||
|
||||
- win_command:
|
||||
cmd: cmd to run here
|
||||
|
||||
3. Using argv - the values will be escaped using C argument rules
|
||||
|
||||
- win_command:
|
||||
argv:
|
||||
- executable
|
||||
- argument 1
|
||||
- argument 2
|
||||
- repeat as needed
|
||||
|
||||
Each of these options are mutually exclusive and at least 1 needs to be specified.
|
||||
#>
|
||||
$filePath = $null
|
||||
$rawCmdLine = if ($module.Params.cmd) {
|
||||
$module.Params.cmd
|
||||
}
|
||||
elseif ($module.Params._raw_params) {
|
||||
$module.Params._raw_params.Trim()
|
||||
}
|
||||
else {
|
||||
$argv = $module.Params.argv
|
||||
|
||||
# First resolve just the executable to an absolute path
|
||||
$filePath = Resolve-ExecutablePath -FilePath $argv[0] -WorkingDirectory $chdir
|
||||
|
||||
# Then combine the executable + remaining arguments and escape them
|
||||
@(
|
||||
ConvertTo-EscapedArgument -InputObject $filePath
|
||||
$argv | Select-Object -Skip 1 | ConvertTo-EscapedArgument
|
||||
) -join " "
|
||||
}
|
||||
|
||||
$module.Result.cmd = $rawCmdLine
|
||||
$module.Result.rc = 0
|
||||
|
||||
if ($creates -and $(Test-AnsiblePath -Path $creates)) {
|
||||
$module.Result.msg = "skipped, since $creates exists"
|
||||
$module.Result.skipped = $true
|
||||
$module.ExitJson()
|
||||
}
|
||||
|
||||
if ($removes -and -not $(Test-AnsiblePath -Path $removes)) {
|
||||
$module.Result.msg = "skipped, since $removes does not exist"
|
||||
$module.Result.skipped = $true
|
||||
$module.ExitJson()
|
||||
}
|
||||
|
||||
$commandParams = @{
|
||||
CommandLine = $rawCmdLine
|
||||
ZuulLogId = $zuul_log_id
|
||||
ZuulLogPath = "C:/ProgramData/Zuul/Zuul/console-$zuul_log_id.log"
|
||||
ZuulOutputMaxBytes = $zuul_output_max_bytes
|
||||
}
|
||||
if ($filePath) {
|
||||
$commandParams.FilePath = $filePath
|
||||
}
|
||||
if ($chdir) {
|
||||
$commandParams.WorkingDirectory = $chdir
|
||||
}
|
||||
if ($stdin) {
|
||||
$commandParams.InputObject = $stdin
|
||||
}
|
||||
if ($output_encoding_override) {
|
||||
$commandParams.OutputEncodingOverride = $output_encoding_override
|
||||
}
|
||||
|
||||
$startDatetime = [DateTime]::UtcNow
|
||||
try {
|
||||
$cmdResult = Start-AnsibleWindowsProcess @commandParams
|
||||
}
|
||||
catch {
|
||||
$module.Result.rc = 2
|
||||
|
||||
# Keep on checking inner exceptions to see if it has the NativeErrorCode to
|
||||
# report back.
|
||||
$exp = $_.Exception
|
||||
while ($exp) {
|
||||
if ($exp.PSObject.Properties.Name -contains 'NativeErrorCode') {
|
||||
$module.Result.rc = $exp.NativeErrorCode
|
||||
break
|
||||
}
|
||||
$exp = $exp.InnerException
|
||||
}
|
||||
|
||||
$module.FailJson("Failed to run: '$rawCmdLine': $($_.Exception.Message)", $_)
|
||||
}
|
||||
|
||||
$module.Result.cmd = $cmdResult.Command
|
||||
$module.Result.changed = $true
|
||||
$module.Result.stdout = $cmdResult.Stdout
|
||||
$module.Result.stderr = $cmdResult.Stderr
|
||||
$module.Result.rc = $cmdResult.ExitCode
|
||||
|
||||
$endDatetime = [DateTime]::UtcNow
|
||||
$module.Result.start = $startDatetime.ToString("yyyy-MM-dd HH:mm:ss.ffffff")
|
||||
$module.Result.end = $endDatetime.ToString("yyyy-MM-dd HH:mm:ss.ffffff")
|
||||
$module.Result.delta = $($endDatetime - $startDatetime).ToString("h\:mm\:ss\.ffffff")
|
||||
|
||||
If ($module.Result.rc -ne 0) {
|
||||
$module.FailJson("non-zero return code")
|
||||
}
|
||||
|
||||
$module.ExitJson()
|
152
zuul/ansible/base/library/win_shell.ps1
Normal file
152
zuul/ansible/base/library/win_shell.ps1
Normal file
@ -0,0 +1,152 @@
|
||||
#!powershell
|
||||
|
||||
# This is based on: https://github.com/ansible-collections/ansible.windows/blob/2.5.0/plugins/modules/win_shell.ps1
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.Zuul.Win.Shell.CommandUtil
|
||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||
|
||||
# TODO: add check mode support
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Cleanse CLIXML from stderr (sift out error stream data, discard others for now)
|
||||
Function Format-Stderr($raw_stderr) {
|
||||
Try {
|
||||
# NB: this regex isn't perfect, but is decent at finding CLIXML amongst other stderr noise
|
||||
If ($raw_stderr -match "(?s)(?<prenoise1>.*)#< CLIXML(?<prenoise2>.*)(?<clixml><Objs.+</Objs>)(?<postnoise>.*)") {
|
||||
$clixml = [xml]$matches["clixml"]
|
||||
$filtered = $clixml.Objs.ChildNodes |
|
||||
Where-Object { $_.Name -eq 'S' } |
|
||||
Where-Object { $_.S -eq 'Error' } |
|
||||
ForEach-Object { $_.'#text'.Replace('_x000D__x000A_', '') } |
|
||||
Out-String
|
||||
|
||||
$merged_stderr = "{0}{1}{2}{3}" -f @(
|
||||
$matches["prenoise1"],
|
||||
$matches["prenoise2"],
|
||||
# filter out just the Error-tagged strings for now, and zap embedded CRLF chars
|
||||
$filtered,
|
||||
$matches["postnoise"]) | Out-String
|
||||
|
||||
return $merged_stderr.Trim()
|
||||
|
||||
# FUTURE: parse/return other streams
|
||||
}
|
||||
Else {
|
||||
$raw_stderr
|
||||
}
|
||||
}
|
||||
Catch {
|
||||
"***EXCEPTION PARSING CLIXML: $_***" + $raw_stderr
|
||||
}
|
||||
}
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $false
|
||||
|
||||
$raw_command_line = Get-AnsibleParam -obj $params -name "_raw_params" -type "str" -failifempty $true
|
||||
$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path"
|
||||
$executable = Get-AnsibleParam -obj $params -name "executable" -type "path"
|
||||
$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
|
||||
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
|
||||
$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
|
||||
$no_profile = Get-AnsibleParam -obj $params -name "no_profile" -type "bool" -default $false
|
||||
$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str"
|
||||
$zuul_log_id = Get-AnsibleParam -obj $params -name "zuul_log_id" -type "str"
|
||||
$zuul_output_max_bytes = Get-AnsibleParam -obj $params -name "zuul_output_max_bytes" -type "int"
|
||||
|
||||
$raw_command_line = $raw_command_line.Trim()
|
||||
|
||||
$result = @{
|
||||
changed = $true
|
||||
cmd = $raw_command_line
|
||||
}
|
||||
|
||||
if ($creates -and $(Test-AnsiblePath -Path $creates)) {
|
||||
Exit-Json @{ msg = "skipped, since $creates exists"; cmd = $raw_command_line; changed = $false; skipped = $true; rc = 0 }
|
||||
}
|
||||
|
||||
if ($removes -and -not $(Test-AnsiblePath -Path $removes)) {
|
||||
Exit-Json @{ msg = "skipped, since $removes does not exist"; cmd = $raw_command_line; changed = $false; skipped = $true; rc = 0 }
|
||||
}
|
||||
|
||||
$exec_args = $null
|
||||
If (-not $executable -or $executable -eq "powershell") {
|
||||
$exec_application = "powershell.exe"
|
||||
|
||||
# force input encoding to preamble-free UTF8 so PS sub-processes (eg, Start-Job) don't blow up
|
||||
$raw_command_line = "[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false; " + $raw_command_line
|
||||
|
||||
# Base64 encode the command so we don't have to worry about the various levels of escaping
|
||||
$encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_command_line))
|
||||
|
||||
if ($stdin) {
|
||||
$exec_args = "-encodedcommand $encoded_command"
|
||||
}
|
||||
else {
|
||||
$exec_args = "-noninteractive -encodedcommand $encoded_command"
|
||||
}
|
||||
|
||||
if ($no_profile) {
|
||||
$exec_args = "-noprofile $exec_args"
|
||||
}
|
||||
}
|
||||
Else {
|
||||
# FUTURE: support arg translation from executable (or executable_args?) to process arguments for arbitrary interpreter?
|
||||
$exec_application = $executable
|
||||
if (-not ($exec_application.EndsWith(".exe"))) {
|
||||
$exec_application = "$($exec_application).exe"
|
||||
}
|
||||
$exec_args = "/c $raw_command_line"
|
||||
}
|
||||
|
||||
$command = "`"$exec_application`" $exec_args"
|
||||
$run_command_arg = @{
|
||||
command = $command
|
||||
zuul_log_id = $zuul_log_id
|
||||
zuul_log_path = "C:/ProgramData/Zuul/Zuul/console-$zuul_log_id.log"
|
||||
zuul_output_max_bytes = $zuul_output_max_bytes
|
||||
}
|
||||
if ($chdir) {
|
||||
$run_command_arg['working_directory'] = $chdir
|
||||
}
|
||||
if ($stdin) {
|
||||
$run_command_arg['stdin'] = $stdin
|
||||
}
|
||||
if ($output_encoding_override) {
|
||||
$run_command_arg['output_encoding_override'] = $output_encoding_override
|
||||
}
|
||||
|
||||
$start_datetime = [DateTime]::UtcNow
|
||||
try {
|
||||
$command_result = Run-Command @run_command_arg
|
||||
}
|
||||
catch {
|
||||
$result.changed = $false
|
||||
try {
|
||||
$result.rc = $_.Exception.NativeErrorCode
|
||||
}
|
||||
catch {
|
||||
$result.rc = 2
|
||||
}
|
||||
Fail-Json -obj $result -message $_.Exception.Message
|
||||
}
|
||||
|
||||
# TODO: decode CLIXML stderr output (and other streams?)
|
||||
$result.stdout = $command_result.stdout
|
||||
$result.stderr = Format-Stderr $command_result.stderr
|
||||
$result.rc = $command_result.rc
|
||||
|
||||
$end_datetime = [DateTime]::UtcNow
|
||||
$result.start = $start_datetime.ToString("yyyy-MM-dd HH:mm:ss.ffffff")
|
||||
$result.end = $end_datetime.ToString("yyyy-MM-dd HH:mm:ss.ffffff")
|
||||
$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
|
||||
|
||||
If ($result.rc -ne 0) {
|
||||
Fail-Json -obj $result -message "non-zero return code"
|
||||
}
|
||||
|
||||
Exit-Json $result
|
69
zuul/ansible/base/library/win_zuul_console.ps1
Normal file
69
zuul/ansible/base/library/win_zuul_console.ps1
Normal file
@ -0,0 +1,69 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright (C) 2025 Acme Gating, LLC
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType
|
||||
|
||||
# We put the logs in this location to approximate the global /tmp on
|
||||
# unix. We can't use the individual user's home directory because it
|
||||
# may not be writeable if "become" is used.
|
||||
|
||||
$LOG_STREAM_FILE = "C:/ProgramData/Zuul/Zuul/console-{0}.log"
|
||||
$LOG_STREAM_PORT = 19886
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
_zuul_console_exec_path = @{ type = "str" }
|
||||
path = @{ type = "str"; default = $LOG_STREAM_FILE }
|
||||
port = @{ type = "int"; default = $LOG_STREAM_PORT }
|
||||
state = @{ type = "str"; choices = "absent", "present" }
|
||||
}
|
||||
}
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
function Base64-Encode {
|
||||
param ($Str)
|
||||
[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Str))
|
||||
}
|
||||
|
||||
# The log path can have {} characters in it for python string
|
||||
# interpolation; avoid shell issues by base64 encoding it.
|
||||
$LogPath = Base64-Encode $module.Params.path
|
||||
|
||||
if ($module.Params.state -eq "absent") {
|
||||
try {
|
||||
# Identify the process by port and kill it.
|
||||
Get-Process -Id (Get-NetTCPConnection -LocalPort $module.Params.port).OwningProcess | Stop-Process
|
||||
$module.Result.changed = $true
|
||||
} catch {}
|
||||
} else {
|
||||
try {
|
||||
# If we can find a process listening on this port, assume it
|
||||
# is a pre-existing daemon and skip the rest.
|
||||
Get-Process -Id (Get-NetTCPConnection -LocalPort $module.Params.port).OwningProcess | Out-Null
|
||||
} catch {
|
||||
$ConsoleCommand = "powershell -executionpolicy bypass -File $($module.Params._zuul_console_exec_path) $($LogPath) $($module.Params.port)"
|
||||
# This method of starting a new process completely detaches it
|
||||
# from the ssh process.
|
||||
Invoke-WmiMethod -Path 'Win32_Process' -Name Create -ArgumentList $ConsoleCommand | Out-Null
|
||||
$module.Result.changed = $true
|
||||
# Return the resulting command for debugging.
|
||||
$module.Result.cmd = $ConsoleCommand
|
||||
}
|
||||
}
|
||||
|
||||
$module.ExitJson()
|
351
zuul/ansible/win_zuul_console.cs
Normal file
351
zuul/ansible/win_zuul_console.cs
Normal file
@ -0,0 +1,351 @@
|
||||
// Copyright (c) 2016 IBM Corp.
|
||||
// Copyright (C) 2025 Acme Gating, LLC
|
||||
|
||||
// This module is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This software is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
// This file follows the structure of win_console.py closely (meaning
|
||||
// some of this code is more pythonic than C#-like).
|
||||
|
||||
internal class ZuulConsole : IDisposable
|
||||
{
|
||||
public string path;
|
||||
public FileStream file;
|
||||
public long size;
|
||||
|
||||
public ZuulConsole(string path)
|
||||
{
|
||||
this.path = path;
|
||||
this.file = new FileStream(
|
||||
path, FileMode.Open, FileAccess.Read,
|
||||
FileShare.ReadWrite | FileShare.Delete);
|
||||
this.size = new FileInfo(path).Length;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
file.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class Server
|
||||
{
|
||||
private const int MAX_REQUEST_LEN = 1024;
|
||||
private const int REQUEST_TIMEOUT = 10;
|
||||
// This is the version we report to the zuul_stream callback. It is
|
||||
// expected that this (zuul_console) process can be long-lived, so if
|
||||
// there are updates this ensures a later streaming callback can still
|
||||
// talk to us.
|
||||
private const int ZUUL_CONSOLE_PROTO_VERSION = 1;
|
||||
|
||||
private string path;
|
||||
private Socket socket;
|
||||
|
||||
public Server(string path, int port)
|
||||
{
|
||||
this.path = path;
|
||||
socket = new Socket(
|
||||
AddressFamily.InterNetworkV6,
|
||||
SocketType.Stream,
|
||||
ProtocolType.Tcp);
|
||||
socket.SetSocketOption(
|
||||
SocketOptionLevel.Socket,
|
||||
SocketOptionName.ReuseAddress, true);
|
||||
socket.SetSocketOption(
|
||||
SocketOptionLevel.IPv6,
|
||||
SocketOptionName.IPv6Only, false);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, port));
|
||||
socket.Listen(1);
|
||||
}
|
||||
|
||||
private Socket Accept()
|
||||
{
|
||||
return socket.Accept();
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Socket conn = Accept();
|
||||
Thread t = new Thread(StartHandleOneConnection);
|
||||
t.IsBackground = true;
|
||||
t.Start((object) conn);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartHandleOneConnection(object conn)
|
||||
{
|
||||
try
|
||||
{
|
||||
HandleOneConnection((Socket) conn);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("Error in connection handler: {0}",
|
||||
e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private ZuulConsole ChunkConsole(Socket conn, string logUuid)
|
||||
{
|
||||
ZuulConsole console;
|
||||
try
|
||||
{
|
||||
console = new ZuulConsole(string.Format(path, logUuid));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
Byte[] chunk = new Byte[4096];
|
||||
int len = console.file.Read(chunk, 0, 4096);
|
||||
if (len == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
conn.Send(chunk, len, SocketFlags.None);
|
||||
}
|
||||
return console;
|
||||
}
|
||||
|
||||
private bool FollowConsole(ZuulConsole console, Socket conn)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// As long as we have unread data, keep reading/sending
|
||||
while (true)
|
||||
{
|
||||
Byte[] chunk = new Byte[4096];
|
||||
int len = console.file.Read(chunk, 0, 4096);
|
||||
if (len > 0)
|
||||
{
|
||||
conn.Send(chunk, len, SocketFlags.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
// At this point, we are waiting for more data to be written
|
||||
Thread.Sleep(500);
|
||||
|
||||
// Check to see if the remote end has sent any data,
|
||||
// if so, discard
|
||||
if (conn.Poll(0, SelectMode.SelectError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (conn.Poll(0, SelectMode.SelectRead))
|
||||
{
|
||||
Byte[] chunk = new Byte[4096];
|
||||
int ret = conn.Receive(chunk);
|
||||
// Discard anything read, if input is eof, it has
|
||||
// disconnected.
|
||||
if (ret == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
// See if the file has been truncated
|
||||
try
|
||||
{
|
||||
long currentSize = new FileInfo(console.path).Length;
|
||||
if (currentSize < console.size)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
console.size = currentSize;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
private string GetCommand(Socket conn)
|
||||
{
|
||||
Byte[] buff = new Byte[MAX_REQUEST_LEN];
|
||||
DateTime start = DateTime.UtcNow;
|
||||
int pos = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int elapsed = (DateTime.UtcNow - start).Seconds;
|
||||
int timeout = Math.Max(REQUEST_TIMEOUT - elapsed, 0);
|
||||
if (timeout == 0)
|
||||
{
|
||||
throw new Exception("Timeout while waiting for input");
|
||||
}
|
||||
|
||||
if (conn.Poll(0, SelectMode.SelectRead))
|
||||
{
|
||||
int len = conn.Receive(buff, pos, MAX_REQUEST_LEN - pos,
|
||||
SocketFlags.None);
|
||||
if (len == 0) {
|
||||
throw new Exception("Remote side closed connection");
|
||||
}
|
||||
}
|
||||
if (conn.Poll(0, SelectMode.SelectError))
|
||||
{
|
||||
throw new Exception("Received error event");
|
||||
}
|
||||
|
||||
if (pos >= MAX_REQUEST_LEN)
|
||||
{
|
||||
throw new Exception("Request too long");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string ret = System.Text.Encoding.UTF8.GetString(buff);
|
||||
int x = ret.IndexOf('\n');
|
||||
if (x > 0)
|
||||
{
|
||||
return ret.Substring(0,x).Trim();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
private string CleanUuid(string logUuid)
|
||||
{
|
||||
// Make use the input isn't trying to be clever and
|
||||
// construct some path like /tmp/console-/../../something
|
||||
return Path.GetFileName(logUuid);
|
||||
}
|
||||
|
||||
private void HandleOneConnection(Socket conn)
|
||||
{
|
||||
// V1 protocol
|
||||
// -----------
|
||||
// v:<ver> get version number, <ver> is remote version
|
||||
// s:<uuid> send logs for <uuid>
|
||||
// f:<uuid> finalise/cleanup <uuid>
|
||||
string logUuid;
|
||||
|
||||
while (true)
|
||||
{
|
||||
string command = GetCommand(conn);
|
||||
if (command.StartsWith("v:"))
|
||||
{
|
||||
// NOTE(ianw) : remote sends its version. We currently
|
||||
// don't have anything to do with this value, so ignore
|
||||
// for now.
|
||||
conn.Send(
|
||||
System.Text.Encoding.UTF8.GetBytes(
|
||||
String.Format("{0}\n", ZUUL_CONSOLE_PROTO_VERSION)));
|
||||
continue;
|
||||
}
|
||||
else if (command.StartsWith("f:"))
|
||||
{
|
||||
logUuid = CleanUuid(command.Substring(2));
|
||||
try
|
||||
{
|
||||
File.Delete(string.Format(path, logUuid));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (command.StartsWith("s:"))
|
||||
{
|
||||
logUuid = CleanUuid(command.Substring(2));
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// NOTE(ianw): 2022-07-21 In releases < 6.3.0 the streaming
|
||||
// side would just send a raw uuid and nothing else; so by
|
||||
// default assume that is what is coming in here. We can
|
||||
// remove this fallback when we decide it is no longer
|
||||
// necessary.
|
||||
logUuid = CleanUuid(command);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: this won't notice disconnects until it tries to send
|
||||
ZuulConsole console = null;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (console != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
console.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
console = ChunkConsole(conn, logUuid);
|
||||
if (console != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
conn.Send(System.Text.Encoding.UTF8.GetBytes(
|
||||
"[Zuul] Log not found\n"));
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
if (FollowConsole(console, conn))
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
conn.Close();
|
||||
if (console != null)
|
||||
{
|
||||
console.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WinZuulConsole
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
if (args.Length < 2)
|
||||
{
|
||||
throw new Exception("Not enough arguments");
|
||||
}
|
||||
|
||||
string path = System.Text.Encoding.UTF8.GetString(
|
||||
Convert.FromBase64String(args[0]));
|
||||
int port = Int32.Parse(args[1]);
|
||||
|
||||
Server s = new Server(path, port);
|
||||
s.Run();
|
||||
}
|
||||
}
|
@ -406,7 +406,8 @@ class KubeFwd(object):
|
||||
|
||||
def __init__(self, zuul_event_id, build, kubeconfig, context,
|
||||
namespace, pod):
|
||||
self.port = None
|
||||
self.port1 = None
|
||||
self.port2 = None
|
||||
self.fwd = None
|
||||
self.log = get_annotated_logger(
|
||||
logging.getLogger("zuul.ExecutorServer"),
|
||||
@ -415,22 +416,27 @@ class KubeFwd(object):
|
||||
self.context = context
|
||||
self.namespace = namespace
|
||||
self.pod = pod
|
||||
self.socket = None
|
||||
self.socket1 = None
|
||||
self.socket2 = None
|
||||
|
||||
def _getSocket(self):
|
||||
# Reserve a port so that we can restart the forwarder if it
|
||||
# exits, which it will if there is any connection problem at
|
||||
# all.
|
||||
self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.socket.bind(('::', 0))
|
||||
self.port = self.socket.getsockname()[1]
|
||||
# Reserve a port for each of the possible log streaming ports
|
||||
# so that we can restart the forwarder if it exits, which it
|
||||
# will if there is any connection problem at all.
|
||||
self.socket1 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
self.socket1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.socket1.bind(('::', 0))
|
||||
self.port1 = self.socket1.getsockname()[1]
|
||||
self.socket2 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
self.socket2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.socket2.bind(('::', 0))
|
||||
self.port2 = self.socket2.getsockname()[1]
|
||||
|
||||
def start(self):
|
||||
if self.fwd:
|
||||
return
|
||||
|
||||
if self.socket is None:
|
||||
if self.socket1 is None or self.socket2 is None:
|
||||
self._getSocket()
|
||||
|
||||
cmd = [
|
||||
@ -442,7 +448,8 @@ class KubeFwd(object):
|
||||
shlex.quote(self.namespace),
|
||||
'port-forward',
|
||||
shlex.quote('pod/%s' % self.pod),
|
||||
'%s:19885' % self.port,
|
||||
'%s:19885' % self.port1,
|
||||
'%s:19886' % self.port2,
|
||||
';', 'do', ':;', 'done',
|
||||
]
|
||||
cmd = ' '.join(cmd)
|
||||
@ -455,19 +462,21 @@ class KubeFwd(object):
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
stdin=devnull)
|
||||
# This is a quick check to make sure it started correctly, so
|
||||
# we only check the first line and the first port.
|
||||
line = fwd.stdout.readline().decode('utf8')
|
||||
m = re.match(r'^Forwarding from 127.0.0.1:(\d+) -> 19885', line)
|
||||
port = None
|
||||
if m:
|
||||
port = m.group(1)
|
||||
if port != str(self.port):
|
||||
if port != str(self.port1):
|
||||
self.log.error("Could not find the forwarded port: %s", line)
|
||||
self.stop()
|
||||
raise Exception("Unable to start kubectl port forward")
|
||||
self.fwd = fwd
|
||||
pgid = os.getpgid(self.fwd.pid)
|
||||
self.log.info('Started Kubectl port forward on port %s with '
|
||||
'process group %s', self.port, pgid)
|
||||
self.log.info('Started Kubectl port forward on ports %s and %s with '
|
||||
'process group %s', self.port1, self.port2, pgid)
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
@ -488,9 +497,15 @@ class KubeFwd(object):
|
||||
except Exception:
|
||||
self.log.exception('Unable to stop kubectl port-forward:')
|
||||
try:
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
if self.socket1:
|
||||
self.socket1.close()
|
||||
self.socket1 = None
|
||||
except Exception:
|
||||
self.log.exception('Unable to close port-forward socket:')
|
||||
try:
|
||||
if self.socket2:
|
||||
self.socket2.close()
|
||||
self.socket2 = None
|
||||
except Exception:
|
||||
self.log.exception('Unable to close port-forward socket:')
|
||||
|
||||
@ -1112,6 +1127,7 @@ class AnsibleJob(object):
|
||||
plugin_dir = self.executor_server.ansible_manager.getAnsiblePluginDir(
|
||||
self.ansible_version)
|
||||
self.library_dir = os.path.join(plugin_dir, 'library')
|
||||
self.module_utils_dir = os.path.join(self.library_dir, 'module_utils')
|
||||
self.action_dir = os.path.join(plugin_dir, 'action')
|
||||
self.callback_dir = os.path.join(plugin_dir, 'callback')
|
||||
self.lookup_dir = os.path.join(plugin_dir, 'lookup')
|
||||
@ -2822,8 +2838,10 @@ class AnsibleJob(object):
|
||||
try:
|
||||
fwd.start()
|
||||
self.port_forwards.append(fwd)
|
||||
zuul_resources[node.name[0]]['stream_port'] = \
|
||||
fwd.port
|
||||
zuul_resources[node.name[0]]['stream_port1'] = \
|
||||
fwd.port1
|
||||
zuul_resources[node.name[0]]['stream_port2'] = \
|
||||
fwd.port2
|
||||
except Exception:
|
||||
self.log.exception("Unable to start port forward:")
|
||||
self.log.error("Kubectl and socat are required for "
|
||||
@ -3067,8 +3085,8 @@ class AnsibleJob(object):
|
||||
config.write('fact_caching = jsonfile\n')
|
||||
config.write('fact_caching_connection = %s\n' %
|
||||
self.jobdir.fact_cache)
|
||||
config.write('library = %s\n'
|
||||
% self.library_dir)
|
||||
config.write('library = %s\n' % self.library_dir)
|
||||
config.write('module_utils = %s\n' % self.module_utils_dir)
|
||||
config.write('command_warnings = False\n')
|
||||
# Disable the Zuul callback plugins for the freeze playbooks
|
||||
# as that output is verbose and would be confusing for users.
|
||||
|
Loading…
x
Reference in New Issue
Block a user