def test_config_file(self, tmpdir, device_option, overwrite, expected): p = tmpdir.join("tcconfig.json") config = "{" + '"{:s}"'.format(device_option) + ": {" + """ "outgoing": { "network=192.168.0.10/32, port=8080": { "delay": "10.0", "loss": "0.01", "rate": "250K", "delay-distro": "2.0" }, "network=0.0.0.0/0": {} }, "incoming": { "network=192.168.10.0/24": { "corrupt": "0.02", "rate": "1500K" }, "network=0.0.0.0/0": {} } } } """ p.write(config) SubprocessRunner("tcdel --device " + device_option).run() command = " ".join(["tcset -f ", str(p), overwrite]) assert SubprocessRunner(command).run() == expected runner = SubprocessRunner("tcshow --device " + device_option) runner.run() assert json.loads(runner.stdout) == json.loads(config) SubprocessRunner("tcdel --device " + device_option).run()
def __get_ifb_from_device(self): filter_parser = TcFilterParser() command = "tc filter show dev {:s} root".format(self.device) filter_runner = SubprocessRunner(command) filter_runner.run() return filter_parser.parse_incoming_device(filter_runner.stdout)
def __parse_qdisc(device): qdisc_parser = TcQdiscParser() command = "tc qdisc show dev {:s}".format(device) qdisk_show_runner = SubprocessRunner(command) qdisk_show_runner.run() return qdisc_parser.parse(qdisk_show_runner.stdout)
def clear(cls): if not cls.enable: return for mangle in cls.parse(): proc = SubprocessRunner(mangle.to_delete_command()) if proc.run() != 0: raise RuntimeError(str(proc.stderr))
def test_normal(self, device_option): if device_option is None: pytest.skip("device option is null") SubprocessRunner("tcdel --device " + device_option).run() command = " ".join([ "tcset", "--device", device_option, "--delay", "10", "--delay-distro", "2", "--loss", "0.01", "--rate", "0.25M", "--network", "192.168.0.10", "--port", "8080", ]) assert SubprocessRunner(command).run() == 0 command = " ".join([ "tcset", "--device", device_option, "--delay", "1", "--loss", "0.02", "--rate", "500K", "--direction", "incoming", ]) assert SubprocessRunner(command).run() == 0 command = " ".join([ "tcshow", "--device", device_option, ]) runner = SubprocessRunner(command) runner.run() expected = "{" + '"{:s}"'.format(device_option) + ": {" + """ "outgoing": { "network=192.168.0.10/32, port=8080": { "delay": "10.0", "loss": "0.01", "rate": "250K", "delay-distro": "2.0" }, "network=0.0.0.0/0": {} }, "incoming": { "network=0.0.0.0/0": { "delay": "1.0", "loss": "0.02", "rate": "500K" } } } }""" assert json.loads(runner.stdout) == json.loads(expected) SubprocessRunner("tcdel --device " + device_option).run()
def __run(command, regexp, message): proc = SubprocessRunner(command, regexp) if proc.run() == 0: return 0 match = regexp.search(proc.stderr) if match is None: return proc.returncode logger.notice(message) return proc.returncode
def update_help(): for command in ["tcset", "tcdel", "tcshow"]: runner = SubprocessRunner("{:s} -h".format(command)) runner.run(env=dict(os.environ, LC_ALL="C.UTF-8")) help_file_path = "pages/usage/{command:s}/{command:s}_help_output.txt".format( command=command) print(help_file_path) with open(help_file_path, "w") as f: f.write("::\n\n") f.write(indent(runner.stdout, " "))
def main(): proc = SubprocessRunner(["pingparsing", "-h"]) proc.run(env=dict(os.environ, LC_ALL="C.UTF-8")) help_file_path = "pages/usage/cli_help.txt" print(help_file_path) with open(help_file_path, "w") as f: f.write("CLI help\n") f.write("--------------------------------------------\n") f.write("::\n\n") f.write(indent(proc.stdout, " "))
def test_config_file_smoke(self, tmpdir, device_value, overwrite): if device_value is None: pytest.skip("device option is null") p = tmpdir.join("tcconfig.json") config = ("{" + '"{:s}"'.format(device_value) + ": {" + """ "outgoing": { "dst-network=192.168.0.10/32, dst-port=8080, protocol=ip": { "filter_id": "800::800", "delay": "10.0ms", "loss": "0.01%", "rate": "250Kbps", "delay-distro": "2.0ms" }, "src-port=1234, protocol=ip": { "filter_id": "800::801", "delay": "50.0ms", "rate": "1Gbps" } }, "incoming": { "dst-network=192.168.10.0/24, protocol=ip": { "filter_id": "800::800", "corrupt": "0.02%", "rate": "1500Kbps" } } } } """) print("[config]\n{}\n".format(config)) p.write(config) for device_option in [ device_value, "--device {}".format(device_value) ]: execute_tcdel(device_value) command = " ".join( [Tc.Command.TCSET, str(p), "--import-setting", overwrite]) SubprocessRunner(command).run() runner = SubprocessRunner("{:s} {:s}".format( Tc.Command.TCSHOW, device_option)) runner.run() print_test_result(expected=config, actual=runner.stdout, error=runner.stderr) assert json.loads(runner.stdout) == json.loads(config) execute_tcdel(device_value)
def test_normal_include_envs(self, tmpdir): first_dir, second_dir = make_dirs(tmpdir) runner = SubprocessRunner([ MODULE, "-f", str(first_dir), str(second_dir), "--debug", "--include-envs" ]) assert runner.run() == 0, runner.stderr print_result(stdout=runner.stdout, stderr=runner.stderr) assert re.search("removed 2 directories", runner.stderr) is not None
def test_normal_exceed_max_rate(self, monkeypatch, device_option, rate): if device_option is None: pytest.skip("device option is null") for tc_target in [device_option]: monkeypatch.setattr("tcconfig._network._read_iface_speed", lambda x: "1") runner = SubprocessRunner([Tc.Command.TCSET, tc_target, "--rate", rate, "--overwrite"]) assert runner.run() == 0, (runner.command_str, runner.returncode, runner.stderr) # finalize --- execute_tcdel(tc_target)
def test_config_file_smoke(self, tmpdir, device_value, overwrite): if device_value is None: pytest.skip("device option is null") p = tmpdir.join("tcconfig.json") config = "{" + '"{:s}"'.format(device_value) + ": {" + """ "outgoing": { "dst-network=192.168.0.10/32, dst-port=8080, protocol=ip": { "filter_id": "800::800", "delay": "10.0ms", "loss": 0.01, "rate": "250K", "delay-distro": "2.0ms" }, "src-port=1234, protocol=ip": { "filter_id": "800::801", "delay": "50.0ms", "rate": "1G" } }, "incoming": { "dst-network=192.168.10.0/24, protocol=ip": { "filter_id": "800::800", "corrupt": 0.02, "rate": "1500K" } } } } """ print("[config]\n{}\n".format(config)) p.write(config) device_option = "--device {:s}".format(device_value) execute_tcdel(device_value) command = " ".join( ["{:s} -f ".format(Tc.Command.TCSET), str(p), overwrite]) SubprocessRunner(command).run() runner = SubprocessRunner("{:s} {:s}".format(Tc.Command.TCSHOW, device_option)) runner.run() print("[expected]\n{}\n".format(config)) print("[actual]\n{}\n".format(runner.stdout)) print("[stderr]\n{}".format(runner.stderr)) assert json.loads(runner.stdout) == json.loads(config) execute_tcdel(device_value)
def test_normal_single(self, tmpdir): tmp_ping_file = tmpdir.join("ping_deb.txt") tmp_ping_file.write(DEBIAN_SUCCESS_0.value) tmp_ping_path = str(tmp_ping_file) runner = SubprocessRunner(["pingparsing", tmp_ping_path]) runner.run() print_result(stdout=runner.stdout, stderr=runner.stderr) assert runner.returncode == 0 assert json.loads( runner.stdout)[tmp_ping_path] == DEBIAN_SUCCESS_0.expected
def test_normal_list(self, tmpdir): first_dir, second_dir = make_dirs(tmpdir) runner = SubprocessRunner( [MODULE, "--list", str(first_dir), str(second_dir), "-a"]) assert runner.run() == 0, runner.stderr print_result(stdout=runner.stdout, stderr=runner.stderr) targets = runner.stdout.splitlines() assert len(targets) == 6 for target in targets: assert str(tmpdir) in target
def test_normal_single(self): count = 1 dest = "localhost" runner = SubprocessRunner(["pingparsing", dest, "-c", count]) runner.run() print_result(stdout=runner.stdout, stderr=runner.stderr) assert runner.returncode == 0 parsed_result = json.loads(runner.stdout) assert parsed_result[dest]["packet_transmit"] == count assert parsed_result[dest]["rtt_max"] > 0
def test_normal_single_dir(self, tmpdir): p = tmpdir.mkdir("__pycache__").join("dummy.pyc") p.write("dummy") p = tmpdir.join("test.pyc") p.write("dummy") runner = SubprocessRunner([MODULE, "-f", str(tmpdir), "-v"]) assert runner.run() == 0, runner.stderr print_result(stdout=runner.stdout, stderr=runner.stderr) assert re.search("removed 1 directories", runner.stderr) is not None assert re.search("removed 1 files", runner.stderr) is not None
def test_normal_multi(self): count = 1 dest_list = ["google.com", "twitter.com"] runner = SubprocessRunner(["pingparsing"] + dest_list + ["-c", count]) runner.run() print_result(stdout=runner.stdout, stderr=runner.stderr) assert runner.returncode == 0 parsed_result = json.loads(runner.stdout) for dest in dest_list: assert parsed_result[dest]["packet_transmit"] == count assert parsed_result[dest]["rtt_max"] > 0
def test_normal_single(self, tmpdir): tmp_ping_file = tmpdir.join("ping_deb.txt") tmp_ping_file.write(DEBIAN_SUCCESS_0.value) tmp_ping_path = str(tmp_ping_file) runner = SubprocessRunner("pingparsing {}".format(tmp_ping_path)) runner.run() print("[stdout]\n{}".format(runner.stdout)) print("[stderr]\n{}".format(runner.stderr)) assert runner.returncode == 0 assert json.loads( runner.stdout)[tmp_ping_path] == DEBIAN_SUCCESS_0.expected
def test_abnormal(self, monkeypatch, device_option, rate): if device_option is None: pytest.skip("device option is null") for tc_target in [device_option]: monkeypatch.setattr("tcconfig._network._read_iface_speed", lambda x: "1") runner = SubprocessRunner(" ".join( [Tc.Command.TCSET, tc_target, "--rate", rate, "--overwrite"])) assert runner.run() != 0, (runner.command_str, runner.returncode, runner.stderr) # finalize --- delete_all_rules(tc_target)
def test_normal_tc_command(self, tmpdir, device_value): if device_value is None: pytest.skip("device option is null") p = tmpdir.join("tcconfig.json") config = make_config(device_value) print("[config]\n{}\n".format(config)) p.write(config) for device_option in [device_value]: runner = SubprocessRunner( [Tc.Command.TCSET, "--import-setting", str(p), "--tc-command"]) assert runner.run() == 0 assert len(runner.stdout.splitlines()) > 10
def cmd(self, command): status = None executable = command.split() exec_exists = self._check_cmd(executable[0]) if exec_exists: runner = SubprocessRunner(command) runit = runner.run() if runit == 0: status = runner.stdout else: logging.error(f"{executable[0]} command not found") return status
def test_normal_w_option(self, tmpdir): expected = dedent("""\ { "destination": "google.com", "packet_transmit": 3, "packet_receive": 3, "packet_loss_count": 0, "packet_loss_rate": 0.0, "rtt_min": 48.832, "rtt_avg": 54.309, "rtt_max": 64.334, "rtt_mdev": 7.098, "packet_duplicate_count": 0, "packet_duplicate_rate": 0.0, "icmp_replies": [ { "timestamp": null, "icmp_seq": 1, "ttl": 50, "time": 64.3, "duplicate": false }, { "timestamp": null, "icmp_seq": 2, "ttl": 50, "time": 49.7, "duplicate": false }, { "timestamp": null, "icmp_seq": 3, "ttl": 50, "time": 48.8, "duplicate": false } ] } """) runner = SubprocessRunner(["pingparsing", "-", "--icmp-reply"]) runner.run(input=UBUNTU_SUCCESS_2.value) print_result(stdout=runner.stdout, stderr=runner.stderr, expected=expected) assert runner.returncode == 0 assert json.loads(runner.stdout) == json.loads(expected)
def test_const_packet_duplicate(self, device_option, dst_host_option, transmitter, pingparser, option, value): if typepy.is_null_string(dst_host_option): # alternative to pytest.mark.skipif return execute_tcdel(device_option) transmitter.destination_host = dst_host_option # w/o packet duplicate tc --- result = transmitter.ping() pingparser.parse(result.stdout) without_tc_duplicate_rate = pingparser.packet_duplicate_rate # w/ packet duplicate tc --- command_list = [ TcCommand.TCSET, "--device {:s}".format(device_option), "{:s} {:f}".format(option, value), ] assert SubprocessRunner(" ".join(command_list)).run() == 0 result = transmitter.ping() pingparser.parse(result.stdout) with_tc_duplicate_rate = pingparser.packet_duplicate_rate # assertion --- duplicate_rate_diff = with_tc_duplicate_rate - without_tc_duplicate_rate assert duplicate_rate_diff > (value / 2) # finalize --- execute_tcdel(device_option)
def add_filter(self): command_list = [ "tc filter add", self.dev, "protocol ip", "parent {:s}:".format(self._tc_obj.qdisc_major_id_str), "prio 1", ] if self._is_use_iptables(): command_list.append("handle {:d} fw".format( self._get_unique_mangle_mark_id())) else: if dataproperty.is_empty_string(self._tc_obj.network): network = ANYWHERE_NETWORK else: network = self._tc_obj.network command_list.extend([ "u32", "match ip {:s} {:s}".format(self._get_network_direction_str(), network), ]) if self._tc_obj.port is not None: command_list.append("match ip dport {:d} 0xffff".format( self._tc_obj.port)) command_list.append("flowid {:s}:{:d}".format( self._tc_obj.qdisc_major_id_str, self.get_qdisc_minor_id())) return SubprocessRunner(" ".join(command_list)).run()
def __set_network_filter(self, qdisc_major_id): command_list = [ "tc filter add", "dev " + self.__get_tc_device(), "protocol ip", "parent {:x}:".format(qdisc_major_id), "prio 1", ] if self.__is_use_iptables(): mark_id = (IptablesMangleController.get_unique_mark_id() + self.__FILTER_IPTABLES_MARK_ID_OFFSET) command_list.append("handle {:d} fw".format(mark_id)) self.__add_mangle_mark(mark_id) else: if all([ dataproperty.is_empty_string(self.network), self.port is None, ]): return 0 command_list.append("u32") if dataproperty.is_not_empty_string(self.network): command_list.append("match ip {:s} {:s}".format( self.__get_network_direction_str(), self.network)) if self.port is not None: command_list.append("match ip dport {:d} 0xffff".format( self.port)) command_list.append("flowid {:x}:{:d}".format( qdisc_major_id, self.__get_qdisc_minor_id())) return SubprocessRunner(" ".join(command_list)).run()
def test_dst_net_packet_duplicate(self, device_option, dst_host_option, transmitter, pingparser, option, value): if typepy.is_null_string(dst_host_option): pytest.skip("destination host is null") for tc_target in [device_option, "--device {}".format(device_option)]: execute_tcdel(tc_target) transmitter.destination_host = dst_host_option # w/o packet duplicate tc --- ping_result = transmitter.ping() assert ping_result.returncode == 0 without_tc_duplicate_rate = pingparser.parse( ping_result).packet_duplicate_rate # w/ packet duplicate tc --- assert (SubprocessRunner(" ".join( [Tc.Command.TCSET, tc_target, "{:s} {}".format(option, value)])).run() == 0) ping_result = transmitter.ping() assert ping_result.returncode == 0 with_tc_duplicate_rate = pingparser.parse( ping_result).packet_duplicate_rate # assertion --- duplicate_rate_diff = with_tc_duplicate_rate - without_tc_duplicate_rate assert duplicate_rate_diff > (convert_rate_to_f(value) * ASSERT_MARGIN) # finalize --- execute_tcdel(tc_target)
def test_normal_multi(self): count = 1 dest_list = ["google.com", "twitter.com"] runner = SubprocessRunner("pingparsing {:s} -c {:d}".format( " ".join(dest_list), count)) runner.run() print("[stdout]\n{}".format(runner.stdout)) print("[stderr]\n{}".format(runner.stderr)) assert runner.returncode == 0 parsed_result = json.loads(runner.stdout) for dest in dest_list: assert parsed_result[dest]["packet_transmit"] == count assert parsed_result[dest]["rtt_max"] > 0
def test_normal_single(self): count = 1 dest = "localhost" runner = SubprocessRunner("pingparsing {:s} -c {:d}".format( dest, count)) runner.run() print("[stdout]\n{}".format(runner.stdout)) print("[stderr]\n{}".format(runner.stderr)) assert runner.returncode == 0 parsed_result = json.loads(runner.stdout) assert parsed_result[dest]["packet_transmit"] == count assert parsed_result[dest]["rtt_max"] > 0
def test_const_latency(self, device_option, dst_host_option, transmitter, pingparser, delay): if device_option is None: pytest.skip("device option is null") if typepy.is_null_string(dst_host_option): pytest.skip("destination host is null") execute_tcdel(device_option) transmitter.destination_host = dst_host_option # w/o latency tc --- result = transmitter.ping() pingparser.parse(result.stdout) without_tc_rtt_avg = pingparser.rtt_avg # w/ latency tc --- command_list = [ TcCommand.TCSET, "--device " + device_option, "--delay {:d}".format(delay), ] assert SubprocessRunner(" ".join(command_list)).run() == 0 result = transmitter.ping() pingparser.parse(result.stdout) with_tc_rtt_avg = pingparser.rtt_avg # assertion --- rtt_diff = with_tc_rtt_avg - without_tc_rtt_avg assert rtt_diff > (delay / 2.0) # finalize --- execute_tcdel(device_option)
def test_dst_net_packet_duplicate(self, device_option, dst_host_option, transmitter, pingparser, option, value): if typepy.is_null_string(dst_host_option): pytest.skip("destination host is null") execute_tcdel(device_option) transmitter.destination_host = dst_host_option # w/o packet duplicate tc --- ping_result = transmitter.ping() assert ping_result.returncode == 0 pingparser.parse(ping_result) without_tc_duplicate_rate = pingparser.packet_duplicate_rate # w/ packet duplicate tc --- assert SubprocessRunner([ Tc.Command.TCSET, "--device {:s}".format(device_option), "{:s} {:f}".format(option, value), ]).run() == 0 ping_result = transmitter.ping() assert ping_result.returncode == 0 pingparser.parse(ping_result) with_tc_duplicate_rate = pingparser.packet_duplicate_rate # assertion --- duplicate_rate_diff = (with_tc_duplicate_rate - without_tc_duplicate_rate) assert duplicate_rate_diff > (value * ASSERT_MARGIN) # finalize --- execute_tcdel(device_option)
def __set_pre_network_filter(self): if self._is_use_iptables(): return 0 if all( [ typepy.is_null_string(self._tc_obj.dst_network), not typepy.Integer(self._tc_obj.dst_port).is_type(), ] ): flowid = "{:s}:{:d}".format(self._tc_obj.qdisc_major_id_str, self._get_qdisc_minor_id()) else: flowid = "{:s}:2".format(self._tc_obj.qdisc_major_id_str) return SubprocessRunner( " ".join( [ self._tc_obj.get_tc_command(TcSubCommand.FILTER), self._dev, "protocol {:s}".format(self._tc_obj.protocol), "parent {:s}:".format(self._tc_obj.qdisc_major_id_str), "prio 2 u32 match {:s} {:s} {:s}".format( self._tc_obj.protocol, self._get_network_direction_str(), get_anywhere_network(self._tc_obj.ip_version), ), "flowid {:s}".format(flowid), ] ) ).run()
def add(cls, mangling_mark): if not cls.enable: return 0 cls.__check_execution_authority() return SubprocessRunner(mangling_mark.to_append_command()).run()
def main(): options = parse_option() set_log_level(options.log_level) subprocrunner.Which("tc").verify() subprocrunner.SubprocessRunner.is_save_history = True if options.tc_command_output != TcCoomandOutput.NOT_SET: subprocrunner.SubprocessRunner.is_dry_run = True tc_param = {} for device in options.device: try: verify_network_interface(device) except NetworkInterfaceNotFoundError as e: logger.debug(str(e)) continue tc_param.update(TcShapingRuleParser(device, logger).get_tc_parameter()) command_history = "\n".join(SubprocessRunner.get_history()) if options.tc_command_output == TcCoomandOutput.STDOUT: print(command_history) return 0 if options.tc_command_output == TcCoomandOutput.SCRIPT: write_tc_script("tcshow", command_history) return 0 logger.debug("command history\n{}".format(command_history)) print(json.dumps(tc_param, indent=4)) return 0
def test_dst_net_uniform_latency(self, device_option, dst_host_option, transmitter, pingparser, shaping_algo, delay): if device_option is None: pytest.skip("device option is null") if typepy.is_null_string(dst_host_option): pytest.skip("destination host is null") for tc_target in [device_option, "--device {}".format(device_option)]: execute_tcdel(tc_target) transmitter.destination_host = dst_host_option # w/o latency tc --- ping_result = transmitter.ping() assert ping_result.returncode == 0 without_tc_rtt_avg = pingparser.parse(ping_result).rtt_avg # w/ latency tc --- assert (SubprocessRunner([ Tc.Command.TCSET, tc_target, "--delay {}ms".format(delay), "--shaping-algo {:s}".format(shaping_algo), ]).run() == 0) ping_result = transmitter.ping() assert ping_result.returncode == 0 with_tc_rtt_avg = pingparser.parse(ping_result).rtt_avg # assertion --- rtt_diff = with_tc_rtt_avg - without_tc_rtt_avg assert rtt_diff > (delay * ASSERT_MARGIN) # finalize --- execute_tcdel(device_option)
def test_dst_net_packet_loss(self, device_option, dst_host_option, transmitter, pingparser, option, value): if typepy.is_null_string(dst_host_option): pytest.skip("destination host is null") for tc_target in [device_option, "--device {}".format(device_option)]: execute_tcdel(tc_target) transmitter.destination_host = dst_host_option # w/o traffic shaping --- ping_result = transmitter.ping() assert ping_result.returncode == 0 without_tc_loss_rate = pingparser.parse( ping_result).packet_loss_rate # w/ traffic shaping --- assert (SubprocessRunner([ Tc.Command.TCSET, tc_target, "{:s} {:f}".format(option, value) ]).run() == 0) ping_result = transmitter.ping() assert ping_result.returncode == 0 with_tc_loss_rate = pingparser.parse(ping_result).packet_loss_rate # check packet loss rate --- loss_diff = with_tc_loss_rate - without_tc_loss_rate assert loss_diff > (value * ASSERT_MARGIN) # finalize --- execute_tcdel(tc_target)
def __get_filter(self, device): if dataproperty.is_empty_string(device): return {} # parse filter --- filter_parser = TcFilterParser() command = "tc filter show dev {:s}".format(device) filter_show_runner = SubprocessRunner(command) filter_show_runner.run() filter_table = {} for filter_param in filter_parser.parse_filter(filter_show_runner.stdout): filter_key = self.__get_filter_key(filter_param) filter_table[filter_key] = {} if self.__qdisc_param.get("parent") in (filter_param.get("flowid"), filter_param.get("classid")): work_qdisc_param = dict(self.__qdisc_param) del work_qdisc_param["parent"] filter_table[filter_key] = work_qdisc_param return filter_table
""" .. codeauthor:: Tsuyoshi Hombashi <*****@*****.**> """ from __future__ import print_function, unicode_literals import os from textwrap import dedent, indent from subprocrunner import SubprocessRunner env = dict(os.environ, LC_ALL="C.UTF-8") proc = SubprocessRunner("sqlitebiter -h") proc.run(env=env) help_file_path = "pages/usage/help.txt" print(help_file_path) with open(help_file_path, "w") as f: f.write( dedent( """\ :: """ ) ) f.write(indent(proc.stdout, " "))
def get_iptables(cls): proc = SubprocessRunner("iptables -t mangle --line-numbers -L") if proc.run() != 0: raise RuntimeError(str(proc.stderr)) return proc.stdout
#!/usr/bin/env python # encoding: utf-8 from __future__ import print_function, unicode_literals import os from textwrap import indent from subprocrunner import SubprocessRunner proc = SubprocessRunner(["pingparsing", "-h"]) proc.run(env=dict(os.environ, LC_ALL="C.UTF-8")) help_file_path = "pages/usage/cli_help.txt" print(help_file_path) with open(help_file_path, "w") as f: f.write("CLI help\n") f.write("--------------------------------------------\n") f.write("::\n\n") f.write(indent(proc.stdout, " "))